YipyipExAuth.Plugs.ProcessRefreshToken (YipyipExAuth v0.3.1) View Source

Plug to process and verify refresh tokens. Must be initialized with a YipyipExAuth.Config-struct, which can be initialized itself using YipyipExAuth.Config.from_enum/1.

The token signature source (bearer or cookie) must match the token_signature_transport specified in the token payload.

A refresh token can only be used to refresh a session once. A single refresh token id is stored in the server-side session by YipyipExAuth.Plugs.upsert_session/3 to enforce this.

The plug does not reject unauthenticated requests by itself. If a request is successfully verified, the user ID, session ID and extra payload are assigned to the conn. If not, an authentication error message is put in the conn's private map, which can be retrieved using YipyipExAuth.Utils.get_auth_error/1. This allows applications to implement their own plug to reject unauthenticated requests, for example:

Usage example that rejects unauthenticated requests

defmodule MyPhoenixAppWeb.Router do
  use MyPhoenixAppWeb, :router

  @config YipyipExAuth.Config.from_enum(
                           session_ttl: 68400,
                           refresh_token_ttl: 3600,
                           session_store_module: MyModule
                         )

  pipeline :valid_refresh_token do
    plug YipyipExAuth.Plugs.ProcessRefreshToken, @config
    plug :only_authenticated
  end

  @doc """
  Reject unauthenticated requests
  """
  def only_authenticated(%{assigns: %{current_user_id: _}} = conn, _opts), do: conn

  def only_authenticated(conn, _opts) do
    auth_error = YipyipExAuth.Utils.get_auth_error(conn)
    conn |> Plug.Conn.send_resp(401, auth_error) |> halt()
  end
end

In this way, applications can completely customize how to respond to unauthenticated requests and how much information to expose to the client.

Examples / doctests

alias Plug.Conn
alias YipyipExAuth.Plugs.ProcessRefreshToken
alias YipyipExAuth.Utils
import YipyipExAuth.TestHelpers

# only available when Mix env = test
alias YipyipExAuth.TestSupport.FakeSessionStore
import YipyipExAuth.TestSupport.Shared

@config YipyipExAuth.Config.from_enum(
                        session_ttl: 68400,
                        refresh_token_ttl: 3600,
                        session_store_module: FakeSessionStore
                      )
@plug_opts ProcessRefreshToken.init(@config)

# "reject" requests without refresh token
iex> conn = %Conn{} |> ProcessRefreshToken.call(@plug_opts)
iex> "refresh token not found" = Utils.get_auth_error(conn)
iex> {conn.assigns, Utils.get_session(conn)}
{%{}, nil}

# "reject" requests with invalid token
iex> config = %{@config | refresh_token_salt: "different"}
iex> conn = build_conn() |> put_refresh_token(config) |> ProcessRefreshToken.call(@plug_opts)
iex> "refresh token invalid" = Utils.get_auth_error(conn)
iex> {conn.assigns, Utils.get_session(conn)}
{%{}, nil}

# "reject" requests with expired refresh token
iex> plug_opts = ProcessRefreshToken.init(%{@config | refresh_token_ttl: -1})
iex> conn = build_conn() |> put_refresh_token(@config) |> ProcessRefreshToken.call(plug_opts)
iex> "refresh token expired" = Utils.get_auth_error(conn)
iex> {conn.assigns, Utils.get_session(conn)}
{%{}, nil}

# "reject" requests where the signature transport mechanism does not match the session's initial value
iex> token = generate_refresh_token(build_conn(), @config, %{tst: :cookie})
iex> conn = build_conn() |> put_refresh_token(@config, token) |> ProcessRefreshToken.call(@plug_opts)
iex> "token signature transport invalid" = Utils.get_auth_error(conn)
iex> {conn.assigns, Utils.get_session(conn)}
{%{}, nil}

# "reject" requests with an expired session
iex> token = generate_refresh_token(build_conn(), @config, %{exp: 1})
iex> conn = build_conn() |> put_refresh_token(@config, token) |> ProcessRefreshToken.call(@plug_opts)
iex> "session expired" = Utils.get_auth_error(conn)
iex> {conn.assigns, Utils.get_session(conn)}
{%{}, nil}

# "allow" requests with valid refresh token
iex> conn = build_conn() |> put_refresh_token(@config) |> ProcessRefreshToken.call(@plug_opts)
iex> nil = Utils.get_auth_error(conn)
iex> conn.assigns
%{current_session_id: "a", current_user_id: 1, extra_refresh_token_payload: %{}}
iex> %YipyipExAuth.Models.Session{} = Utils.get_session(conn)

# "allow" requests with valid refresh token with signature in cookie
iex> token = generate_refresh_token(build_conn(), @config, %{tst: :cookie})
iex> [header, encoded_payload, signature] = String.split(token, ".", parts: 3)
iex> conn = build_conn()
...> |> put_refresh_token(@config, header <> "." <> encoded_payload)
...> |> Plug.Test.put_req_cookie(@config.refresh_cookie_name, "." <> signature)
...> |> ProcessRefreshToken.call(@plug_opts)
iex> nil = Utils.get_auth_error(conn)
iex> conn.assigns
%{current_session_id: "a", current_user_id: 1, extra_refresh_token_payload: %{}}
iex> %YipyipExAuth.Models.Session{} = Utils.get_session(conn)