# `Charon.TokenPlugs`
[🔗](https://github.com/weareyipyip/charon/blob/v4.3.0/lib/charon/token_plugs.ex#L1)

The plugs in this module (and its submodules) can be used to verify tokens.
The token's presence, signature, expiration and any claims can be checked.
Additionally, the token's session can be loaded and, in case of a refresh token,
it can be verified that it matches the session.

In case of validation errors, the plugs add an "auth error" to the conn,
but don't raise or halt the connection immediately.
This property can be used to support endpoints
that work with- or without authentication, for example,
or if you want to support multiple kinds of tokens.
The plug `verify_no_auth_error/2` can be used to actually do something if there is an error.
All the plugs short-circuit, meaning that they immediately
return the connection if there are errors.

Similarly, in case of successful validation, the token payload, user id and session are not assigned to the conn immediately. You can use `Charon.TokenPlugs.PutAssigns` to customize what is assigned and how.

Using the plugs in these module, you can construct your own verification pipeline
using either Plug.Builder or standard Phoenix router pipelines.
Here are two examples for access- and refresh tokens, respectively,
that should be a good baseline for your own pipelines.

## Access tokens

    defmodule MyApp.AccessTokenPipeline do
      use Plug.Builder

      @config Charon.Config.from_enum(Application.compile_env!(:my_app, :charon))

      plug :get_token_from_auth_header
      plug :get_token_from_cookie, @config.access_cookie_name
      plug :verify_token_signature, @config
      plug :verify_token_nbf_claim
      plug :verify_token_exp_claim
      plug :verify_token_claim_equals, type: "access"
      plug :emit_telemetry
      plug :verify_no_auth_error, &MyApp.TokenErrorHandler.on_error/2
      plug Charon.TokenPlugs.PutAssigns
    end

## Refresh tokens

    defmodule MyApp.RefreshTokenPipeline do
      use Plug.Builder

      @config Charon.Config.from_enum(Application.compile_env!(:my_app, :charon))

      plug :get_token_from_auth_header
      plug :get_token_from_cookie, @config.refresh_cookie_name
      plug :verify_token_signature, @config
      plug :verify_token_nbf_claim
      plug :verify_token_exp_claim
      plug :verify_token_claim_equals, type: "refresh"
      plug :load_session, @config
      plug :verify_token_fresh, 10
      plug :emit_telemetry
      plug :verify_no_auth_error, &MyApp.TokenErrorHandler.on_error/2
      plug Charon.TokenPlugs.PutAssigns
    end

# `claim_and_expectation`

```elixir
@type claim_and_expectation() :: {claim_name(), any()}
```

A single claim name paired with its expected/comparison value.

# `claim_name`

```elixir
@type claim_name() :: String.t() | atom()
```

The name of a token claim to verify.

Atoms are automatically converted to strings (e.g., `:type` becomes `"type"`).
This allows for a more ergonomic API when specifying claims.

# `claims_and_expectations`

```elixir
@type claims_and_expectations() ::
  claim_and_expectation()
  | [claim_and_expectation()]
  | %{required(claim_name()) =&gt; any()}
```

One or more claims with their expected/comparison values.

Can be provided as:
- A single tuple: `{"type", "access"}` or `{:type, "access"}`
- A keyword list: `[type: "access", role: "admin"]`
- A map: `%{type: "access", role: "admin"}`
- A list of tuples: `[{"type", "access"}, {"role", "admin"}]`

# `verifier`

```elixir
@type verifier() :: (Plug.Conn.t(), any() -&gt; Plug.Conn.t() | binary())
```

A verifier function for custom claim validation.

The function receives the connection and the claim value, and must return:
- The connection (possibly modified) if validation succeeds
- An error message string if validation fails

# `emit_telemetry`
*since 4.0.0* 

```elixir
@spec emit_telemetry(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
```

Emit telemetry events after token verification.

Should be placed immediately before `verify_no_auth_error/2` in the pipeline.
See `m:Charon.Telemetry#module-token-events` for details on the emitted events.

# `get_token_from_auth_header`

```elixir
@spec get_token_from_auth_header(Plug.Conn.t(), any()) :: Plug.Conn.t()
```

Get a bearer token from the `authorization` header.

Extracts tokens from headers in the format `"Bearer <token>"` or `"Bearer: <token>"`.
If a valid token is found, it's stored in the connection's private state along with
the transport type (`:bearer`).

## Doctests / examples

    iex> conn()
    ...> |> put_req_header("authorization", "Bearer super.secure.token")
    ...> |> get_token_from_auth_header([])
    ...> |> Utils.get_bearer_token()
    "super.secure.token"

# `get_token_from_cookie`
*since 3.1.0* 

```elixir
@spec get_token_from_cookie(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
```

Get the token or token signature from a cookie.

If a bearer token was previously found by `get_token_from_auth_header/2`, the cookie contents are appended to it if:
- the cookie starts with a dot
- the token ends in a dot (for backwards compatibility)

If no bearer token was previously found, the cookie contents are used as the full token.

## Doctests / examples

    iex> conn()
    ...> |> put_req_cookie("access_cookie", "super.secure.token")
    ...> |> fetch_cookies()
    ...> |> get_token_from_cookie("access_cookie")
    ...> |> Utils.get_bearer_token()
    "super.secure.token"

# `load_session`

```elixir
@spec load_session(Plug.Conn.t(), Charon.Config.t()) :: Plug.Conn.t()
```

Fetch the session to which the bearer token belongs.
Raises on session store error.

Must be used after `verify_token_signature/2`.

Requires the token payload to contain `"sub"`, `"sid"`, and `"styp"` claims.
If the session is found, it's stored in the conn's private state.
If not found, an authentication error is set.

# `maybe_add_error`

# `verify_no_auth_error`

```elixir
@spec verify_no_auth_error(Plug.Conn.t(), (Plug.Conn.t(), String.t() -&gt; Plug.Conn.t())) ::
  Plug.Conn.t()
```

Make sure that no previous plug of this module added an auth error.
In case of an error, `on_error` is called (it should probably halt the connection).

## Doctests / examples

    iex> conn = conn()
    iex> verify_no_auth_error(conn, fn _conn, _error -> raise "BOOM" end)

    # on error, send an error response
    iex> on_error = fn conn, error -> conn |> send_resp(401, error) |> halt() end
    iex> %{halted: true, resp_body: "oops!"} =
    ...> conn()
    ...> |> Utils.set_auth_error("oops!")
    ...> |> verify_no_auth_error(on_error)

# `verify_session_payload`

```elixir
@spec verify_session_payload(Plug.Conn.t(), verifier()) :: Plug.Conn.t()
```

Verify the session payload. The validation function `verifier` receives the conn and the session, and must return the conn or an error message.

Must be used after `load_session/2`.

## Doctests / examples

    iex> verifier = fn conn, session -> if session.uid == 2, do: conn, else: "not user 2" end
    iex> conn()
    ...> |> Utils.set_session(%{uid: 1})
    ...> |> verify_session_payload(verifier)
    ...> |> Utils.get_auth_error()
    "not user 2"

# `verify_token_claim`

```elixir
@spec verify_token_claim(
  Plug.Conn.t(),
  {claim_name(), verifier()}
  | %{required(claim_name()) =&gt; verifier()}
  | keyword(verifier())
) :: Plug.Conn.t()
```

Verify that the bearer token payload contains `claim` and that its value matches `verifier`, which receives the conn and the claim value, and must return the conn or an error message.
Also accepts a map/keyword of claims and verifiers.

Must be used after `verify_token_signature/2`.

## Doctests

    def verify_read_scope(conn, value) do
      if "read" in String.split(value, ","), do: conn, else: "no read scope"
    end

    iex> conn()
    ...> |> Utils.set_token_payload(%{"scope" => "write"})
    ...> |> verify_token_claim(scope: &verify_read_scope/2)
    ...> |> Utils.get_auth_error()
    "no read scope"

# `verify_token_claim_equals`

```elixir
@spec verify_token_claim_equals(Plug.Conn.t(), claims_and_expectations()) ::
  Plug.Conn.t()
```

Verify that the bearer token payload contains `claim` and that its value is `expected`.
Also accepts a map/keyword of claims and expected values.

Must be used after `verify_token_signature/2`.

## Doctests / examples

    iex> conn()
    ...> |> Utils.set_token_payload(%{"type" => "access"})
    ...> |> verify_token_claim_equals(type: "refresh")
    ...> |> Utils.get_auth_error()
    "bearer token claim type invalid"

# `verify_token_claim_in`

```elixir
@spec verify_token_claim_in(Plug.Conn.t(), claims_and_expectations()) :: Plug.Conn.t()
```

Verify that the bearer token payload contains `claim` and that its value is in `expected`.
Also accepts a map/keyword of claims and lists of expected values.

Must be used after `verify_token_signature/2`.

## Doctests / examples

    iex> conn()
    ...> |> Utils.set_token_payload(%{"uid" => 1, "type" => "access"})
    ...> |> verify_token_claim_in(uid: 1..20, type: ["id", "refresh"])
    ...> |> Utils.get_auth_error()
    "bearer token claim type invalid"

# `verify_token_exp_claim`

```elixir
@spec verify_token_exp_claim(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
```

Verify that the bearer token payload contains a non-expired `exp` (expires at) claim.

Must be used after `verify_token_signature/2`.

Note that a token created by `Charon.SessionPlugs.upsert_session/3` is guaranteed
to have an exp claim that does not outlive its underlying session.

Allows for some clock drift (5 seconds).

# `verify_token_fresh`

```elixir
@spec verify_token_fresh(Plug.Conn.t(), pos_integer()) :: Plug.Conn.t()
```

Verify that the token (either access or refresh) is fresh.

A token is fresh if it belongs to the *current or previous* refresh generation.
A generation is a set of tokens that is created within `new_cycle_after` seconds after the first token in the generation is created.
A token created after `new_cycle_after` seconds starts a new generation.

It would be simpler to just have a single fresh (refresh) token. However, because of refresh race conditions caused by
network issues or misbehaving clients, enforcing only a single fresh token causes too many problems in practice.

In addition to this generation mechanism, 5 seconds of clock drift are allowed.

Must be used after `load_session/2`. Verify the token type with `verify_token_claim_equals/2`.

## Freshness example

New cycle is created 5 seconds after the generation's first token is created,
and token TTL is 24h (which is irrelevant in this example).

| Time | Token | Current gen   | Previous gen  | Fresh   | Comment                                                           |
|------|-------|---------------|---------------|---------|-------------------------------------------------------------------|
| 0    | A     | g1 (0): A     | -             | A       | Login, initial token generation g1 of the session is created.     |
| 10   | B     | g2 (10): B    | g1 (0): A     | A, B    | Refresh after g1 expires, g1 becomes prev gen                     |
| 12   | C     | g2 (10): B, C | g1 (0): A     | A, B, C | Refresh before g2 expires, C added to current gen                 |
| 20   | D     | g3 (20): D    | g2 (10): B, C | B, C, D | Refresh after g2 expires, g1 is now stale and g2 becomes prev gen |
| 30   | E     | g4 (30): E    | g3 (20): D    | D, E    | Refresh after g3 expires, g2 is now stale and g3 becomes prev gen |

# `verify_token_nbf_claim`

```elixir
@spec verify_token_nbf_claim(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()
```

Verify that the bearer token payload contains a valid `nbf` (not before) claim.

Must be used after `verify_token_signature/2`.

Allows for some clock drift (5 seconds).

# `verify_token_ordset_claim_contains`

> This function is deprecated. It has been replaced by Charon.TokenPlugs.OrdsetClaimHas, which protects against misconfiguration by initializing comparison values as ordsets.

```elixir
@spec verify_token_ordset_claim_contains(Plug.Conn.t(), claims_and_expectations()) ::
  Plug.Conn.t()
```

Verify that the bearer token payload contains `claim`, *which is assumed to be an `m::ordsets`*,
and that `ordset` (*which is also assumed to be either an ordset or a single element*)
is a subset of that ordset.

> #### Ordset requirement {: .warning}
>
> The verified token claims **and** the comparison value **must** be properly formatted `m::ordsets`.
> The plug does not validate this - malformed values will produce incorrect results or errors.

Also accepts a map/keyword of claims and expected ordsets.

## Doctests / examples

    iex> conn()
    ...> |> Utils.set_token_payload(%{"scope" => ["a", "b", "c"]})
    ...> |> verify_token_ordset_claim_contains({"scope", "a"})
    ...> |> Utils.get_auth_error()
    nil

    iex> conn()
    ...> |> Utils.set_token_payload(%{"scope" => ["a", "b", "c"]})
    ...> |> verify_token_ordset_claim_contains(scope: ["c", "d", "e"])
    ...> |> Utils.get_auth_error()
    "bearer token claim scope does not contain [d, e]"

# `verify_token_payload`

```elixir
@spec verify_token_payload(Plug.Conn.t(), verifier()) :: Plug.Conn.t()
```

Verify the bearer token payload.
The validation function `verifier` receives the conn and the token payload, and must return the conn or an error message.

Must be used after `verify_token_signature/2`.

## Doctests / examples

    iex> verifier = fn conn, payload -> is_map_key(payload, "sub") && conn || "no sub claim" end
    iex> conn()
    ...> |> Utils.set_token_payload(%{"id" => 1})
    ...> |> verify_token_payload(verifier)
    ...> |> Utils.get_auth_error()
    "no sub claim"

# `verify_token_signature`

```elixir
@spec verify_token_signature(Plug.Conn.t(), Charon.Config.t()) :: Plug.Conn.t()
```

Verify that the bearer token found by `get_token_from_auth_header/2` is signed correctly,
using the configured token factory.

If verification succeeds, the token payload is stored in the connection's private state.
If verification fails, an authentication error is set instead.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
