defmodule PasetoPlug do @moduledoc """ Documentation for PasetoPlug. """ import Plug.Conn @type paseto_key :: {binary(), binary()} | binary() @doc """ Main entrypoint for whenever the plug is loaded. It is expected that you will pass in a function capable of returning a binary key (for local) or keypair. If using `v1 local`, you will need to provide a binary key up to 32 bytes long. If using v1 public, you will need to provide a keypair ({binary(), binary()}) that can typically be generated by doing `:crypto.generate_key(:rsa, {2048, 65_537})`. NOTE: The modulus and exponent must be exactly as they are declared here. If using `v2 local`, you will need to provide a binary key that is exactly 64 bytes long. If using `v2 public`, you can generate a keypair doing `{:ok, public_key, secret_key} = Salty.Sign.Ed25519.keypair()` Further information can be found here: `https://github.com/GrappigPanda/paseto` """ @spec init(%{key_provider: (() -> paseto_key)}) :: paseto_key def init(key_provider: key_provider) do key_provider.() end @doc """ Essentially wherever the verification happens. Should everything go according to plan, you will find a new `:claims` key in your `conn.assigns` """ def call(conn, key) when is_binary(key) do do_call(conn, key) end def call(conn, public_key) do do_call(conn, public_key) end defp do_call(conn, key) do conn |> get_auth_token() |> case do {:ok, token} -> validate_token(token, key) error -> error end |> (&create_auth_response(conn, &1)).() end @spec get_auth_token(%Plug.Conn{}) :: {:ok, String.t()} | {:error, String.t()} defp get_auth_token(%Plug.Conn{} = conn) do case get_req_header(conn, "authorization") do ["Bearer " <> token] -> {:ok, String.trim(token)} _error -> {:error, "Invalid Authorization Header. Expected `Authorization: Bearer `"} end end @spec validate_token(String.t(), paseto_key) :: {:ok, %Paseto.Token{}} | {:error, String.t()} defp validate_token(token, key) do token |> Paseto.parse_token(key) end @spec create_auth_response( %Plug.Conn{}, {:ok, %Paseto.V1{} | %Paseto.V2{}} | {:error, String.t()} ) :: any() defp create_auth_response(%Plug.Conn{} = conn, token_validation) do case token_validation do {:ok, token} -> assign(conn, :claims, token) {:error, reason} -> conn |> send_resp(401, reason) |> halt() end end end