lib/assent/jwt_adapter/jose.ex

defmodule Assent.JWTAdapter.JOSE do
  @moduledoc """
  JWT adapter module for parsing JSON Web Tokens with JOSE.

  See `Assent.JWTAdapter` for more.
  """
  alias Assent.JWTAdapter

  @behaviour Assent.JWTAdapter

  @impl JWTAdapter
  def sign(claims, alg, secret, opts) do
    jwk = jwk(alg, secret)
    jws = jws(alg, opts)

    {_, token} =
      jwk
      |> JOSE.JWT.sign(jws, claims)
      |> JOSE.JWS.compact()

    {:ok, token}
  end

  defp jwk("HS" <> _rest, secret), do: JOSE.JWK.from_oct(secret)
  defp jwk(_alg, key) when is_binary(key), do: JOSE.JWK.from_pem(key)
  defp jwk(_alg, key) when is_map(key), do: JOSE.JWK.from_map(key)

  defp jws(alg, opts) do
    jws = %{"alg" => alg}

    case Keyword.get(opts, :private_key_id) do
      nil -> jws
      kid -> Map.put(jws, "kid", kid)
    end
  end

  @impl JWTAdapter
  def verify(token, secret_or_public_key, _opts) do
    {_, %{"alg" => alg} = header} =
      token
      |> JOSE.JWT.peek_protected()
      |> JOSE.JWS.to_map()

    {verified, %{fields: claims}} = verify_message(token, alg, secret_or_public_key)

    {%{}, %{"signature" => signature}} = JOSE.JWS.expand(token)

    {:ok, %{
      header: header,
      claims: claims,
      signature: signature,
      verified?: verified
    }}
  end

  defp verify_message(token, _alg, nil) do
    {false, JOSE.JWT.peek_payload(token)}
  end
  defp verify_message(token, alg, secret_or_public_key) do
    {verified, payload, _} =
      alg
      |> jwk(secret_or_public_key)
      |> JOSE.JWT.verify(token)

    {verified, payload}
  end
end