lib/joken_jwks.ex

defmodule JokenJwks do
  @moduledoc """
  `Joken.Hooks` implementation for fetching `Joken.Signer`s from public JWKS URLs.

  This hook is intended to be used when you are _verifying_ a token is signed with
  a well known public key. It only overrides the `before_verify/2` callback providing a
  `Joken.Signer` for the given token. It is important to notice this is not meant for
  use when **GENERATING** a token. So, using this hook with `Joken.encode_and_sign`
  function **WILL NOT WORK!!!**

  To use it, pass this hook to Joken either with the `add_hook/2` macro or directly
  to each `Joken` function. Example:

      defmodule MyToken do
        use Joken.Config

        add_hook(JokenJwks, strategy: MyFetchingStrategy)

        # rest of your token config
      end

  Or:

      Joken.verify_and_validate(config, token, nil, context, [{Joken.Jwks, strategy: MyStrategy}])

  ## Fetching strategy

  Very rarely, your authentication server might rotate or block its keys. Key rotation is the
  process of issuing a new key that in time will replace the older key. This is security hygiene
  and should/might be a regular process.

  Sometimes it is important to block keys because they got leaked or for any other reason.

  Other times you simply don't control the authentication server and can't ensure the keys won't
  change. This is the most common scenario for this hook.

  In these cases (and some others) it is important to have a cache invalidation strategy: all your
  cached keys should be refreshed. Since the best strategy might differ for each use case, there
  is a behaviour that can be customized as the "fetching strategy", that is: when to fetch and re-fetch
  keys. `JokenJwks` has a default strategy that tries to be smart and cover most use cases by default.
  It combines a time based state machine to avoid overflowing the system with re-fetching keys. If  that
  is not a good option for your use case, it can still be configured. Please, see
  `JokenJwks.SignerMatchStrategy` or `JokenJwks.DefaultStrategyTemplate` docs for more information.
  """

  require Logger

  use Joken.Hooks

  @impl true
  def before_verify(hook_options, {token, _signer}) do
    with strategy <- hook_options[:strategy] || raise("No strategy provided"),
         {:ok, kid} <- get_token_kid(token),
         {:ok, signer} <- strategy.match_signer_for_kid(kid, hook_options) do
      {:cont, {token, signer}}
    else
      err -> {:halt, err}
    end
  end

  defp get_token_kid(token) do
    with {:ok, headers} <- Joken.peek_header(token),
         {:kid, kid} when not is_nil(kid) <- {:kid, headers["kid"]} do
      {:ok, kid}
    else
      {:kid, nil} -> {:error, :no_kid_in_token_header}
      err -> err
    end
  end

  def log(_, :none, _), do: :ok

  def log(:debug, log_level, msg) do
    unless Logger.compare_levels(:debug, log_level) == :lt, do: Logger.debug(fn -> msg end)
  end

  def log(:info, log_level, msg) do
    unless Logger.compare_levels(:info, log_level) == :lt, do: Logger.info(fn -> msg end)
  end

  def log(:warn, log_level, msg) do
    unless Logger.compare_levels(:warn, log_level) == :lt, do: Logger.warn(fn -> msg end)
  end

  def log(:error, _, msg), do: Logger.error(msg)
end