<|start_filename|>lib/asteroid/object_store.ex<|end_filename|>
defmodule Asteroid.ObjectStore do
  @object_store_conf_keys [
    :object_store_access_token,
    :object_store_refresh_token,
    :object_store_authorization_code,
    :object_store_device_code,
    :object_store_request_object,
    :object_store_authenticated_session,
    :object_store_authentication_event
  ]

  import Asteroid.Utils

  require Logger

  @spec auto_install_from_config() :: :ok | {:error, any()}

  def auto_install_from_config() do
    object_store_conf_entries()
    |> do_auto_install_from_config()
  end

  @spec do_auto_install_from_config([Keyword.t()]) :: :ok | {:error, any()}

  defp do_auto_install_from_config([object_store_opts | t]) do
    if object_store_opts[:auto_install] != false do
      Logger.info("#{__MODULE__}: configuring token store `#{object_store_opts[:module]}`")

      case object_store_opts[:module].install(object_store_opts[:opts]) do
        :ok ->
          do_auto_install_from_config(t)

        {:error, _} = error ->
          error
      end
    else
      do_auto_install_from_config(t)
    end
  end

  defp do_auto_install_from_config([]) do
    :ok
  end

  @spec auto_start_from_config() :: :ok | {:error, any()}

  def auto_start_from_config() do
    object_store_conf_entries()
    |> do_auto_start_from_config()
  end

  @spec do_auto_start_from_config([Keyword.t()]) :: :ok | {:error, any()}

  defp do_auto_start_from_config([object_store_opts | t]) do
    if object_store_opts[:auto_start] != false do
      Logger.info("#{__MODULE__}: starting token store `#{object_store_opts[:module]}`")

      impl = object_store_opts[:module]

      if function_exported?(impl, :start_link, 1) do
        case impl.start_link(object_store_opts[:opts] || []) do
          {:ok, _pid} ->
            do_auto_start_from_config(t)

          {:error, _} = error ->
            error
        end
      else
        case impl.start(object_store_opts[:opts] || []) do
          :ok ->
            do_auto_start_from_config(t)

          {:error, _} = error ->
            error
        end
      end
    else
      do_auto_start_from_config(t)
    end
  end

  defp do_auto_start_from_config([]) do
    :ok
  end

  @spec object_store_conf_entries() :: [Keyword.t()]

  defp object_store_conf_entries() do
    Enum.reduce(
      @object_store_conf_keys,
      [],
      fn
        key, acc ->
          case astrenv(key) do
            opts when is_list(opts) ->
              acc ++ [opts]

            nil ->
              acc
          end
      end
    )
  end
end


<|start_filename|>lib/asteroid/oidc/acr.ex<|end_filename|>
defmodule Asteroid.OIDC.ACR do
  @moduledoc """
  Functions to work with OpenID Connect ACRs
  """

  @type config :: [{atom(), [config_option()]}]

  @typedoc """
  # FIXME
  """

  @type config_option ::
          {:callback, AsteroidWeb.AuthorizeController.web_authorization_callback()}
          | {:auth_event_set, [auth_event_set()]}
          | {:default, boolean()}

  @typedoc """
  An authentication event set, containing the names of the events

  For instance: `["password", "otp"]`, `["webauthn", "otp"]`
  """

  @type auth_event_set :: [String.t(), ...]
end


<|start_filename|>lib/asteroid/object_store/refresh_token/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.RefreshToken.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.RefreshToken` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.RefreshToken.opts()`) are:
  - `:table_name`: an `atom()` for the table name. Defaults to `:asteroid_refresh_token`
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased. One can use it to add additional indexes for clients or devices, e.g.:
  `tab_def: [index: :subject_id, :client_id]`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `1200` (20 minutes)

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :subject_id, :client_id, :device_id, :authenticated_session :data],
    disc_copies: [node()]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  alias Asteroid.Token.RefreshToken

  @behaviour Asteroid.ObjectStore.RefreshToken

  @impl true

  def install(opts) do
    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    table_name = opts[:table_name] || :asteroid_refresh_token

    tab_def =
      [
        attributes: [:id, :subject_id, :client_id, :device_id, :authenticated_session, :data],
        disc_copies: [node()]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(table_name, tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created refresh token store #{table_name}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: refresh token store #{table_name} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create refresh token store #{table_name} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 1200], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(refresh_token_id, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    case :mnesia.dirty_read(table_name, refresh_token_id) do
      [] ->
        Logger.debug(
          "#{__MODULE__}: getting refresh token `#{refresh_token_id}`, " <> "value: `nil`"
        )

        {:ok, nil}

      [{^table_name, ^refresh_token_id, _subject_id, _client_id, _device_id, _as, data}] ->
        refresh_token =
          RefreshToken.new(
            id: refresh_token_id,
            data: data
          )

        Logger.debug(
          "#{__MODULE__}: getting refresh token `#{refresh_token_id}`, " <>
            "value: `#{inspect(refresh_token)}`"
        )

        {:ok, refresh_token}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_subject_id(subject_id, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    {:ok,
     for {_table_name, refresh_token_id, _subject_id, _client_id, _device_id, _as, _data} <-
           :mnesia.dirty_match_object({table_name, :_, subject_id, :_, :_, :_, :_}) do
       refresh_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_client_id(client_id, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    {:ok,
     for {_table_name, refresh_token_id, _subject_id, _client_id, _device_id, _as, _data} <-
           :mnesia.dirty_match_object({table_name, :_, :_, client_id, :_, :_, :_}) do
       refresh_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_device_id(device_id, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    {:ok,
     for {_table_name, refresh_token_id, _subject_id, _client_id, _device_id, _as, _data} <-
           :mnesia.dirty_match_object({table_name, :_, :_, :_, device_id, :_, :_}) do
       refresh_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_authenticated_session_id(as_id, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    {:ok,
     for {_table_name, refresh_token_id, _subject_id, _client_id, _device_id, _as, _data} <-
           :mnesia.dirty_match_object({table_name, :_, :_, :_, :_, as_id, :_}) do
       refresh_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(refresh_token, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    record = {
      table_name,
      refresh_token.id,
      refresh_token.data["sub"],
      refresh_token.data["client_id"],
      refresh_token.data["device_id"],
      refresh_token.data["authenticated_session_id"],
      refresh_token.data
    }

    :mnesia.dirty_write(table_name, record)

    Logger.debug(
      "#{__MODULE__}: stored refresh token `#{refresh_token.id}`, " <>
        "value: `#{inspect(refresh_token)}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(refresh_token_id, opts) do
    table_name = opts[:table_name] || :asteroid_refresh_token

    :mnesia.dirty_delete(table_name, refresh_token_id)

    Logger.debug("#{__MODULE__}: deleted refresh token `#{refresh_token_id}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>lib/asteroid/config/builder.ex<|end_filename|>
defmodule Asteroid.Config.Builder do
  defmodule Schema do
    defmacro field(name, options \\ []) do
      quote do
        field_documentation = Module.delete_attribute(__MODULE__, :doc)

        field_documentation =
          case field_documentation do
            {_line, val} ->
              val

            nil ->
              IO.warn(
                "Missing documentation for configuration field `#{unquote(name)}`. Please add it by adding `@doc \"field documentation here\"` above the line where you define it."
              )

              ""
          end

        Module.put_attribute(
          __MODULE__,
          :config_fields,
          %{
            module: __MODULE__,
            name: unquote(name),
            documentation: field_documentation,
            options: unquote(options)
          }
        )
      end
    end
  end

  defmacro defconfig(_options \\ [], do: block) do
    quote do
      import Asteroid.Config.Builder.Schema

      Module.register_attribute(__MODULE__, :config_fields, accumulate: true)

      try do
        unquote(block)
      after
        config_fields =
          Module.get_attribute(__MODULE__, :config_fields)
          |> Enum.reverse()

        {line_number, existing_moduledoc} =
          Module.delete_attribute(__MODULE__, :moduledoc) || {0, ""}

        Module.put_attribute(
          __MODULE__,
          :moduledoc,
          {line_number,
           existing_moduledoc <>
             Asteroid.Config.Builder.__config_doc__(config_fields)}
        )

        # FIXME: dynamically generate types
        # See: https://elixirforum.com/t/dynamically-generate-typespecs-from-module-attribute-list/7078/2
        #
        # Enum.each(
        #  config_fields,
        #  fn
        #    %{options: options, name: name} ->
        #      if options[:type] do
        #        @type unquote(:name) :: non_neg_integer()
        #      end
        #  end
        # )
      end
    end
  end

  def __config_doc__(config_fields) do
    current_env = Mix.env()

    Mix.env(:dev)
    {conf_dev, _imported_paths} = Mix.Config.eval!("config/config.exs")

    Mix.env(:test)
    {conf_test, _imported_path} = Mix.Config.eval!("config/config.exs")

    Mix.env(:prod)
    {conf_prod, _imported_path} = Mix.Config.eval!("config/config.exs")

    Mix.env(current_env)

    field_list =
      config_fields
      |> Enum.sort_by(
        fn %{name: name} -> name end,
        &</2
      )
      |> Enum.reduce(
        "Options:\n",
        fn
          %{name: name}, acc ->
            acc <> "- [`:#{name}`](#module-#{to_string(name)})\n"
        end
      )

    acc =
      Enum.reduce(
        config_fields,
        field_list,
        fn
          %{name: name, documentation: documentation, options: options}, acc ->
            config_time =
              case options[:config_time] do
                :runtime ->
                  "runtime"

                :compile ->
                  "compile-time"

                _ ->
                  "*Unknown*"
              end

            doc = """
            ## `:#{name}`

            ### Properties

            |         Property          |         Value                       |
            |--------------------------:|-------------------------------------|
            | Configuration time:       | #{config_time}                      |
            | Type:                     | `t:#{name}/0`                       |
            | Default value (dev):      | #{default_value(conf_dev, name)}    |
            | Default value (test):     | #{default_value(conf_test, name)}   |
            | Default value (prod):     | #{default_value(conf_prod, name)}   |
            """

            doc =
              if options[:uses] do
                uses =
                  options[:uses]
                  |> Enum.map(&to_string/1)
                  |> Enum.map(fn str -> "[`:" <> str <> "`](#module-" <> str <> ")" end)
                  |> Enum.join("<br/>")

                doc <> "| Uses: | #{uses} |\n"
              else
                doc
              end

            doc =
              if options[:used_by] do
                used_by =
                  options[:used_by]
                  |> Enum.map(&to_string/1)
                  |> Enum.map(fn str -> "[`:" <> str <> "`](#module-" <> str <> ")" end)
                  |> Enum.join("<br/>")

                doc <> "| Used by:| #{used_by}|\n"
              else
                doc
              end

            doc =
              if options[:unit] do
                doc <> "| Unit:| #{options[:unit]}|\n"
              else
                doc
              end

            acc <>
              doc <>
              """
              ### Documentation

              #{documentation || "*No documentation*"}

              ***

              """
        end
      )

    """
    ***

    #{acc}
    """
  end

  @spec default_value(Keyword.t(), atom()) :: String.t()
  defp default_value(conf, key) do
    case conf[:asteroid][key] do
      f when is_function(f) ->
        case inspect(f) do
          "&" <> function_name ->
            "`" <> function_name <> "`"

          val ->
            "`" <> val <> "`"
        end

      val ->
        "`#{inspect(val)}`"
    end
  end
end


<|start_filename|>lib/asteroid/oidc/authenticated_session.ex<|end_filename|>
defmodule Asteroid.OIDC.AuthenticatedSession do
  @moduledoc """
  Convenience functions to work with authenticated sessions

  The `%Asteroid.OIDC.AuthenticatedSession{}` object has the following meaningful members in
  its `:data` field:
  - `"current_acr"`: the current ACR, as calculated (`t:Asteroid.OIDC.acr/0`)
  - `"current_auth_time"`: the current authentication time, as calculated (`non_neg_integer`)
  - `"exp"`: expiration time (`non_neg_integer()`), to be optionally used if authentication
  events are not
  """

  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.OIDC
  alias Asteroid.OIDC.AuthenticationEvent
  alias Asteroid.Subject
  alias Asteroid.Token
  alias Asteroid.Token.RefreshToken

  @type id :: String.t()

  @enforce_keys [:id, :subject_id]

  defstruct [:id, :subject_id, :data]

  @type t :: %__MODULE__{
          id: id(),
          subject_id: Subject.id(),
          data: map()
        }

  @doc """
  Generates a new authenticated session
  """

  @spec gen_new(Subject.id()) :: t()

  def gen_new(subject_id) do
    %__MODULE__{
      id: secure_random_b64(),
      subject_id: subject_id,
      data: %{}
    }
  end

  @doc """
  Gets an authenticated session from the store
  """

  @spec get(id(), Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}

  def get(authenticated_session_id, _opts \\ []) do
    as_store_module = astrenv(:object_store_authenticated_session)[:module]
    as_store_opts = astrenv(:object_store_authenticated_session)[:opts] || []

    case as_store_module.get(authenticated_session_id, as_store_opts) do
      {:ok, authenticated_session} when not is_nil(authenticated_session) ->
        {:ok, authenticated_session}

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "authenticated session",
           reason: "not found in the store",
           id: authenticated_session_id
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Stores an authenticated session
  """

  @spec store(t(), Context.t()) :: {:ok, t()} | {:error, any()}

  def store(authenticated_session, ctx \\ %{})

  def store(authenticated_session, ctx) do
    as_store_module = astrenv(:object_store_authenticated_session)[:module]
    as_store_opts = astrenv(:object_store_authenticated_session)[:opts] || []

    authenticated_session =
      astrenv(:object_store_authenticated_session_before_store_callback).(
        authenticated_session,
        ctx
      )

    case as_store_module.put(authenticated_session, as_store_opts) do
      :ok ->
        {:ok, authenticated_session}

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Deletes an authenticated session
  """

  @spec delete(t() | id()) :: :ok | {:error, any()}

  def delete(%__MODULE__{id: id}) do
    delete(id)
  end

  def delete(authenticated_session_id) do
    as_store_module = astrenv(:object_store_authenticated_session)[:module]
    as_store_opts = astrenv(:object_store_authenticated_session)[:opts] || []

    as_store_module.delete(authenticated_session_id, as_store_opts)

    rt_store_module = astrenv(:object_store_refresh_token)[:module]
    rt_store_opts = astrenv(:object_store_refresh_token)[:opts] || []

    case rt_store_module.get_from_authenticated_session_id(
           authenticated_session_id,
           rt_store_opts
         ) do
      {:ok, refresh_token_ids} ->
        for refresh_token_id <- refresh_token_ids do
          {:ok, refresh_token} = RefreshToken.get(refresh_token_id, check_active: false)

          rt_scopes = refresh_token.data["scope"] || []

          if "openid" in rt_scopes and "offline_access" not in rt_scopes do
            RefreshToken.delete(refresh_token_id)
          end
        end

        :ok

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Puts a value into the `data` field of authenticated session

  If the value is `nil`, the authenticated session is not changed and the filed is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(authenticated_session, _key, nil), do: authenticated_session

  def put_value(authenticated_session, key, val) do
    %{authenticated_session | data: Map.put(authenticated_session.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of a authenticated session

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(authenticated_session, key) do
    %{authenticated_session | data: Map.delete(authenticated_session.data, key)}
  end

  @doc """
  Computes the current ACR of an authenticated session
  """

  @spec compute_current_acr(%__MODULE__{} | id()) :: OIDC.acr() | nil

  def compute_current_acr(%__MODULE__{id: id}) do
    compute_current_acr(id)
  end

  def compute_current_acr(authenticated_session_id) when is_binary(authenticated_session_id) do
    ae_store_module = astrenv(:object_store_authentication_event)[:module]
    ae_store_opts = astrenv(:object_store_authentication_event)[:opts] || []

    case ae_store_module.get_from_authenticated_session_id(
           authenticated_session_id,
           ae_store_opts
         ) do
      {:ok, auth_event_ids} ->
        auth_events =
          auth_event_ids
          |> Enum.map(fn
            auth_event_id ->
              {:ok, auth_event} = AuthenticationEvent.get(auth_event_id)

              auth_event
          end)
          |> Enum.reduce(MapSet.new(), &MapSet.put(&2, &1.data["name"]))

        acr_config =
          Enum.find(
            astrenv(:oidc_acr_config, []),
            fn
              {_acr, acr_conf} ->
                Enum.find(
                  acr_conf[:auth_event_set] || [],
                  fn
                    acr_auth_events when is_list(acr_auth_events) ->
                      acr_auth_events = MapSet.new(acr_auth_events)

                      MapSet.subset?(acr_auth_events, auth_events)
                  end
                )
            end
          )

        case acr_config do
          {acr, _} ->
            Atom.to_string(acr)

          nil ->
            nil
        end

      _ ->
        nil
    end
  end

  @doc """
  Updates the current acr of an authenticated session and stores it
  """

  @spec update_acr(%__MODULE__{} | id()) :: {:ok, %__MODULE__{}} | {:error, any()}

  def update_acr(%__MODULE__{id: id}) do
    update_acr(id)
  end

  def update_acr(authenticated_session_id) when is_binary(authenticated_session_id) do
    {:ok, auth_session} = get(authenticated_session_id)

    case compute_current_acr(authenticated_session_id) do
      nil ->
        delete(authenticated_session_id)

      acr when is_binary(acr) ->
        auth_session
        |> put_value("current_acr", acr)
        |> store()
    end
  end

  @doc """
  Returns the authentication time and the AMRs of an authenticated session for a given ACR

  If no ACR is given as the second argument, returns the current state of the session.

  It returns:
  - `:acr`: the acr
  - `:auth_time`: the timestamp of the most recent authentication event
  - `:amr`: the list of ARMs of these authentication events

  Returns `nil` if no match was found.

  ## Example
  ```elixir
  iex> Asteroid.Utils.astrenv(:oidc_acr_config)
  [
    "3-factor": [
      callback: &AsteroidWeb.LOA3_webflow.start_webflow/2,
      auth_event_set: [["password", "otp", "webauthn"]]
    ],
    "2-factor": [
      callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
      auth_event_set: [
        ["password", "<PASSWORD>"],
        ["password", "<PASSWORD>"],
        ["webauthn", "otp"]
      ]
    ],
    "1-factor": [
      callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
      auth_event_set: [["password"], ["webauthn"]],
      default: true
    ]
  ]
  iex> alias Asteroid.OIDC.AuthenticationEvent, as: AE
  Asteroid.OIDC.AuthenticationEvent
  iex> alias Asteroid.OIDC.AuthenticatedSession, as: AS
  Asteroid.OIDC.AuthenticatedSession
  iex> {:ok, as} = AS.gen_new("user_1") |> AS.store()
  {:ok,
   %Asteroid.OIDC.AuthenticatedSession{
     data: %{},
     id: "gvycEhbeig9RjwUu36UEGTxO1MNRE3qQ9WHisfpk0Zk",
     subject_id: "user_1"
   }}
  iex> AE.gen_new(as.id) |> AE.put_value("name", "password") |> AE.put_value("amr", "pwd") |> AE.put_value("time", 100000) |> AE.store()
  {:ok,
   %Asteroid.OIDC.AuthenticationEvent{
     authenticated_session_id: "gvycEhbeig9RjwUu36UEGTxO1MNRE3qQ9WHisfpk0Zk",
     data: %{"amr" => "pwd", "name" => "password", "time" => 100000},
     id: "WxQ6AHMRthQlk9cqsGUMVWsFNZ3EeNjyFfNCRYkiF20"
   }}
  iex> AE.gen_new(as.id) |> AE.put_value("name", "otp") |> AE.put_value("amr", "otp") |> AE.put_value("time", 200000)|> AE.store()
  {:ok,
   %Asteroid.OIDC.AuthenticationEvent{
     authenticated_session_id: "gvycEhbeig9RjwUu36UEGTxO1MNRE3qQ9WHisfpk0Zk",
     data: %{"amr" => "otp", "name" => "otp", "time" => 200000},
     id: "QnZZE82I4St41JieLpLg8z3HG_T8l6yutlt3dPo_Yx8"
   }}
  iex> AE.gen_new(as.id) |> AE.put_value("name", "webauthn") |> AE.put_value("amr", "phr") |> AE.put_value("time", 300000)|> AE.store()
  {:ok,
   %Asteroid.OIDC.AuthenticationEvent{
     authenticated_session_id: "gvycEhbeig9RjwUu36UEGTxO1MNRE3qQ9WHisfpk0Zk",
     data: %{"amr" => "phr", "name" => "webauthn", "time" => 300000},
     id: "N_V4i9lz5obd-3C0XZagZGtOFuDMZo0ywXSBjoum0KY"
   }}
  iex> AS.info(as.id)            
  %{acr: "3-factor", amr: ["otp", "phr", "pwd"], auth_time: 300000}
  iex> AS.info(as.id, "1-factor")
  %{acr: "1-factor", amr: ["pwd"], auth_time: 100000}
  iex> AS.info(as.id, "2-factor")
  %{acr: "2-factor", amr: ["otp", "pwd"], auth_time: 200000}
  iex> AS.info(as.id, "3-factor")
  %{acr: "3-factor", amr: ["otp", "phr", "pwd"], auth_time: 300000}
  ```
  """

  @spec info(%__MODULE__{} | id(), Asteroid.OIDC.acr() | nil) ::
          %{
            required(:acr) => Asteroid.OIDC.acr() | nil,
            required(:auth_time) => non_neg_integer() | nil,
            required(:amr) => [Asteroid.OIDC.amr(), ...]
          }
          | nil

  def info(auth_session_id, acr \\ nil)

  def info(auth_session_id, acr) when is_binary(auth_session_id) do
    case get(auth_session_id) do
      {:ok, auth_session} ->
        info(auth_session, acr)

      {:error, _} ->
        nil
    end
  end

  def info(authenticated_session, acr) do
    acr =
      if acr do
        acr
      else
        authenticated_session.data["current_acr"]
      end

    if acr do
      auth_events =
        AuthenticationEvent.get_from_authenticated_session_id(authenticated_session.id)

      case find_matching_auth_event_set(auth_events, acr) do
        auth_event_set when is_list(auth_event_set) ->
          amr =
            Enum.reduce(
              auth_events,
              MapSet.new(),
              fn
                %AuthenticationEvent{data: %{"name" => name, "amr" => amr}}, acc ->
                  if name in auth_event_set do
                    MapSet.put(acc, amr)
                  else
                    acc
                  end

                _, acc ->
                  acc
              end
            )
            |> MapSet.to_list()

          auth_time =
            Enum.reduce(
              auth_events,
              nil,
              fn
                %AuthenticationEvent{data: %{"name" => name, "time" => auth_time}}, acc ->
                  if name in auth_event_set do
                    if acc == nil do
                      auth_time
                    else
                      max(acc, auth_time)
                    end
                  else
                    acc
                  end

                _, acc ->
                  acc
              end
            )

          %{acr: acr, amr: amr, auth_time: auth_time}

        nil ->
          %{acr: acr, amr: nil, auth_time: nil}
      end
    else
      %{acr: nil, amr: nil, auth_time: nil}
    end
  end

  @doc """
  Find matching authentication event set (`t:Asteroid.OIDC.ACR.auth_event_set/0`) for a given
  ACR from a set of authentication events, or `nil` if none could be found

  ## Example

  ```elixir
  iex> Asteroid.Utils.astrenv(:oidc_acr_config)
  [
    loa2: [
      callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
      auth_event_set: [
        ["password", "<PASSWORD>"],
        ["password", "<PASSWORD>"],
        ["webauthn", "otp"]
      ]
    ],
    loa1: [
      callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
      auth_event_set: [["password"], ["webauthn"]],
      default: true
    ]
  ]
  iex> alias Asteroid.OIDC.AuthenticationEvent
  Asteroid.OIDC.AuthenticationEvent
  iex> alias Asteroid.OIDC.AuthenticatedSession
  Asteroid.OIDC.AuthenticatedSession
  iex> {:ok, as} = AuthenticatedSession.gen_new("user_1") |> AuthenticatedSession.store()
  {:ok,
   %Asteroid.OIDC.AuthenticatedSession{
     data: %{},
     id: "jcKc4uwqDYN7c84B7gzfzgVBkpLMUBNusTRMG6NdOTg",
     subject_id: "user_1"
   }}
  iex> {:ok, ae1} = AuthenticationEvent.gen_new(as.id) |> AuthenticationEvent.put_value("name", "password") |> AuthenticationEvent.store()
  {:ok,
   %Asteroid.OIDC.AuthenticationEvent{
     authenticated_session_id: "jcKc4uwqDYN7c84B7gzfzgVBkpLMUBNusTRMG6NdOTg",
     data: %{"name" => "password"},
     id: "zwhSZ4HPs6JuFpeoTCNNV1wCzFBnnrKAU5bv1FCxLDg"
   }}
  iex> {:ok, ae2} = AuthenticationEvent.gen_new(as.id) |> AuthenticationEvent.put_value("name", "otp") |> AuthenticationEvent.store()
  {:ok,
   %Asteroid.OIDC.AuthenticationEvent{
     authenticated_session_id: "jcKc4uwqDYN7c84B7gzfzgVBkpLMUBNusTRMG6NdOTg",
     data: %{"name" => "otp"},
     id: "cvYtgORDaZfzjxxpUnpS0mf3M1d2lLt74tm6KXifyYg"
   }}
  iex> AuthenticatedSession.find_matching_auth_event_set(AuthenticationEvent.get_from_authenticated_session_id(as.id), "loa1")
  ["password"]
  iex> AuthenticatedSession.find_matching_auth_event_set(AuthenticationEvent.get_from_authenticated_session_id(as.id), "loa2")
  ["password", "<PASSWORD>"]
  iex> AuthenticatedSession.find_matching_auth_event_set(AuthenticationEvent.get_from_authenticated_session_id(as.id), "loa3")
  nil
  ```
  """

  @spec find_matching_auth_event_set([AuthenticationEvent.t()], OIDC.acr()) ::
          OIDC.ACR.auth_event_set()
          | nil

  def find_matching_auth_event_set(auth_events, acr) do
    acr = String.to_existing_atom(acr)

    searched_auth_events_set =
      Enum.reduce(auth_events, MapSet.new(), &MapSet.put(&2, &1.data["name"]))

    Enum.find(
      astrenv(:oidc_acr_config)[acr][:auth_event_set] || [],
      fn
        conf_auth_event_set ->
          conf_auth_event_set = MapSet.new(conf_auth_event_set)

          MapSet.subset?(conf_auth_event_set, searched_auth_events_set)
      end
    )
  rescue
    _ ->
      nil
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oidc/userinfo_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.UserinfoControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias Asteroid.Subject
  alias Asteroid.Token.AccessToken
  alias AsteroidWeb.Router.Helpers, as: Routes

  setup_all do
    :ok =
      Subject.gen_new(id: "user_userinfo_test")
      |> Subject.add("sub", "user_userinfo_test")
      |> Subject.add("name", "<NAME>")
      |> Subject.add("family_name", "Name")
      |> Subject.add("given_name", "Full")
      |> Subject.add("profile", "https://www.example.com/profiles/full_name")
      |> Subject.add("picture", "https://www.example.com/pictures/full_name")
      |> Subject.add("gender", "male")
      |> Subject.add("birthdate", DateTime.from_iso8601("2015-01-23T00:00:00Z") |> elem(1))
      |> Subject.add("updated_at", DateTime.from_iso8601("2019-07-17T21:02:47Z") |> elem(1))
      |> Subject.add("email", "<EMAIL>")
      |> Subject.add("email_verified", true)
      |> Subject.add("address", %{
        "street_address" => "42, Sir Example Street",
        "locality" => "St. Exampleburg",
        "postal_code" => "77852",
        "country" => "Groland"
      })
      |> Subject.add("phone_number", "+3942390027")
      |> Subject.add("non_standard_claim_1", "some value")
      |> Subject.store()

    rsa_enc_alg_all =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:enc)

    Client.gen_new(id: "client_userinfo_sig")
    |> Client.add("client_id", "client_userinfo_sig")
    |> Client.add("client_type", "confidential")
    |> Client.add("userinfo_signed_response_alg", "RS384")
    |> Client.add(
      "jwks",
      [rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)]
    )
    |> Client.store()

    Client.gen_new(id: "client_userinfo_enc")
    |> Client.add("client_id", "client_userinfo_enc")
    |> Client.add("client_type", "confidential")
    |> Client.add("userinfo_signed_response_alg", "RS384")
    |> Client.add("userinfo_encrypted_response_alg", "RSA1_5")
    |> Client.add("userinfo_encrypted_response_enc", "A128GCM")
    |> Client.add(
      "jwks",
      [rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)]
    )
    |> Client.store()

    %{rsa_enc_alg_all: rsa_enc_alg_all}
  end

  test "Success case - requesting using all scopes values, get req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["profile", "email", "address", "phone"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = json_response(conn, 200)

    assert "application/json" in simplified_content_type_from_conn(conn)
    assert response["sub"] == "user_userinfo_test"
    assert response["name"] == "<NAME>"
    assert response["family_name"] == "Name"
    assert response["given_name"] == "Full"
    refute Map.has_key?(response, "middle_name")
    refute Map.has_key?(response, "nickname")
    refute Map.has_key?(response, "preferred_username")
    assert response["profile"] == "https://www.example.com/profiles/full_name"
    assert response["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(response, "website")
    assert response["gender"] == "male"
    assert response["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(response, "zoneinfo")
    refute Map.has_key?(response, "locale")
    assert response["updated_at"] == "2019-07-17T21:02:47Z"
    assert response["email"] == "<EMAIL>"
    assert response["email_verified"] == true

    assert response["address"] == %{
             "street_address" => "42, Sir Example Street",
             "locality" => "St. Exampleburg",
             "postal_code" => "77852",
             "country" => "Groland"
           }

    assert response["phone_number"] == "+3942390027"
    refute Map.has_key?(response, "phone_number_verified")
    refute Map.has_key?(response, "non_standard_claim_1")
  end

  test "Success case - requesting using all scopes values, post req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["profile", "email", "address", "phone"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.store()

    conn =
      post(
        conn,
        Routes.userinfo_path(conn, :show),
        %{"access_token" => AccessToken.serialize(access_token)}
      )

    response = json_response(conn, 200)

    assert "application/json" in simplified_content_type_from_conn(conn)
    assert response["sub"] == "user_userinfo_test"
    assert response["name"] == "<NAME>"
    assert response["family_name"] == "Name"
    assert response["given_name"] == "Full"
    refute Map.has_key?(response, "middle_name")
    refute Map.has_key?(response, "nickname")
    refute Map.has_key?(response, "preferred_username")
    assert response["profile"] == "https://www.example.com/profiles/full_name"
    assert response["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(response, "website")
    assert response["gender"] == "male"
    assert response["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(response, "zoneinfo")
    refute Map.has_key?(response, "locale")
    assert response["updated_at"] == "2019-07-17T21:02:47Z"
    assert response["email"] == "<EMAIL>"
    assert response["email_verified"] == true

    assert response["address"] == %{
             "street_address" => "42, Sir Example Street",
             "locality" => "St. Exampleburg",
             "postal_code" => "77852",
             "country" => "Groland"
           }

    assert response["phone_number"] == "+3942390027"
    refute Map.has_key?(response, "phone_number_verified")
    refute Map.has_key?(response, "non_standard_claim_1")
  end

  test "Success case - requesting using only profile and phone, get req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["profile", "phone"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = json_response(conn, 200)

    assert "application/json" in simplified_content_type_from_conn(conn)
    assert response["sub"] == "user_userinfo_test"
    assert response["name"] == "Full Jr. Name"
    assert response["family_name"] == "Name"
    assert response["given_name"] == "Full"
    refute Map.has_key?(response, "middle_name")
    refute Map.has_key?(response, "nickname")
    refute Map.has_key?(response, "preferred_username")
    assert response["profile"] == "https://www.example.com/profiles/full_name"
    assert response["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(response, "website")
    assert response["gender"] == "male"
    assert response["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(response, "zoneinfo")
    refute Map.has_key?(response, "locale")
    assert response["updated_at"] == "2019-07-17T21:02:47Z"
    refute Map.has_key?(response, "email")
    refute Map.has_key?(response, "email_verified")
    refute Map.has_key?(response, "address")
    assert response["phone_number"] == "+3942390027"
    refute Map.has_key?(response, "phone_number_verified")
    refute Map.has_key?(response, "non_standard_claim_1")
  end

  test "Success case - requesting with no scopes, get req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = json_response(conn, 200)

    assert "application/json" in simplified_content_type_from_conn(conn)
    assert response == %{"sub" => "user_userinfo_test"}
  end

  test "Success case - requesting using all scopes values, signed resp, get req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["profile", "email", "address", "phone"])
      |> AccessToken.put_value("client_id", "client_userinfo_sig")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = response(conn, 200)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, payload_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], response)

    payload = Jason.decode!(payload_str)

    assert "application/jwt" in simplified_content_type_from_conn(conn)
    assert payload["iss"] == OAuth2.issuer()
    assert payload["aud"] == "client_userinfo_sig"
    assert payload["sub"] == "user_userinfo_test"
    assert payload["name"] == "<NAME>"
    assert payload["family_name"] == "Name"
    assert payload["given_name"] == "Full"
    refute Map.has_key?(payload, "middle_name")
    refute Map.has_key?(payload, "nickname")
    refute Map.has_key?(payload, "preferred_username")
    assert payload["profile"] == "https://www.example.com/profiles/full_name"
    assert payload["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(payload, "website")
    assert payload["gender"] == "male"
    assert payload["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(payload, "zoneinfo")
    refute Map.has_key?(payload, "locale")
    assert payload["updated_at"] == "2019-07-17T21:02:47Z"
    assert payload["email"] == "<EMAIL>"
    assert payload["email_verified"] == true

    assert payload["address"] == %{
             "street_address" => "42, Sir Example Street",
             "locality" => "St. Exampleburg",
             "postal_code" => "77852",
             "country" => "Groland"
           }

    assert payload["phone_number"] == "+3942390027"
    refute Map.has_key?(payload, "phone_number_verified")
    refute Map.has_key?(payload, "non_standard_claim_1")
  end

  test "Success case - requesting using all scopes values, signed and encrypted resp, get req",
       %{conn: conn, rsa_enc_alg_all: rsa_enc_alg_all} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["profile", "email", "address", "phone"])
      |> AccessToken.put_value("client_id", "client_userinfo_enc")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = response(conn, 200)

    {payload_signed, _jwe} = JOSE.JWE.block_decrypt(rsa_enc_alg_all, response)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, payload_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], payload_signed)

    payload = Jason.decode!(payload_str)

    assert "application/jwt" in simplified_content_type_from_conn(conn)
    assert payload["iss"] == OAuth2.issuer()
    assert payload["aud"] == "client_userinfo_enc"
    assert payload["sub"] == "user_userinfo_test"
    assert payload["name"] == "Full Jr. Name"
    assert payload["family_name"] == "Name"
    assert payload["given_name"] == "Full"
    refute Map.has_key?(payload, "middle_name")
    refute Map.has_key?(payload, "nickname")
    refute Map.has_key?(payload, "preferred_username")
    assert payload["profile"] == "https://www.example.com/profiles/full_name"
    assert payload["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(payload, "website")
    assert payload["gender"] == "male"
    assert payload["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(payload, "zoneinfo")
    refute Map.has_key?(payload, "locale")
    assert payload["updated_at"] == "2019-07-17T21:02:47Z"
    assert payload["email"] == "<EMAIL>"
    assert payload["email_verified"] == true

    assert payload["address"] == %{
             "street_address" => "42, Sir Example Street",
             "locality" => "St. Exampleburg",
             "postal_code" => "77852",
             "country" => "Groland"
           }

    assert payload["phone_number"] == "+3942390027"
    refute Map.has_key?(payload, "phone_number_verified")
    refute Map.has_key?(payload, "non_standard_claim_1")
  end

  test "Success case - requesting using claims parameter, get req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("__asteroid_oidc_claims", %{
        "userinfo" => %{
          "given_name" => nil,
          "picture" => %{"essential" => true},
          "birthdate" => %{"essential" => false},
          "email" => %{"value" => "<EMAIL>"},
          "non_standard_claim_1" => %{"values" => ["some_value_1", "some_value_2"]}
        },
        "id_token" => %{
          "nickname" => nil,
          "gender" => %{"essential" => true, "values" => ["male", "female"]}
        }
      })
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = json_response(conn, 200)

    assert "application/json" in simplified_content_type_from_conn(conn)
    assert response["sub"] == "user_userinfo_test"
    refute Map.has_key?(response, "name")
    refute Map.has_key?(response, "family_name")
    assert response["given_name"] == "Full"
    refute Map.has_key?(response, "middle_name")
    refute Map.has_key?(response, "nickname")
    refute Map.has_key?(response, "preferred_username")
    refute Map.has_key?(response, "profile")
    assert response["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(response, "website")
    refute Map.has_key?(response, "gender")
    assert response["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(response, "zoneinfo")
    refute Map.has_key?(response, "locale")
    refute Map.has_key?(response, "updated_at")
    assert response["email"] == "<EMAIL>"
    refute Map.has_key?(response, "email_verified")
    refute Map.has_key?(response, "address")
    refute Map.has_key?(response, "phone_number")
    refute Map.has_key?(response, "phone_number_verified")
    assert response["non_standard_claim_1"] == "some value"
  end

  test "Success case - requesting using claims parameter and scopes, get req", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["phone", "email"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("sub", "user_userinfo_test")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("__asteroid_oidc_claims", %{
        "userinfo" => %{
          "given_name" => nil,
          "picture" => %{"essential" => true},
          "birthdate" => %{"essential" => false},
          "email" => %{"value" => "<EMAIL>"},
          "non_standard_claim_1" => %{"values" => ["some_value_1", "some_value_2"]}
        },
        "id_token" => %{
          "nickname" => nil,
          "gender" => %{"essential" => true, "values" => ["male", "female"]}
        }
      })
      |> AccessToken.store()

    conn =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> get(Routes.userinfo_path(conn, :show))

    response = json_response(conn, 200)

    assert "application/json" in simplified_content_type_from_conn(conn)
    assert response["sub"] == "user_userinfo_test"
    refute Map.has_key?(response, "name")
    refute Map.has_key?(response, "family_name")
    assert response["given_name"] == "Full"
    refute Map.has_key?(response, "middle_name")
    refute Map.has_key?(response, "nickname")
    refute Map.has_key?(response, "preferred_username")
    refute Map.has_key?(response, "profile")
    assert response["picture"] == "https://www.example.com/pictures/full_name"
    refute Map.has_key?(response, "website")
    refute Map.has_key?(response, "gender")
    assert response["birthdate"] == "2015-01-23T00:00:00Z"
    refute Map.has_key?(response, "zoneinfo")
    refute Map.has_key?(response, "locale")
    refute Map.has_key?(response, "updated_at")
    assert response["email"] == "<EMAIL>"
    assert response["email_verified"] == true
    refute Map.has_key?(response, "address")
    assert response["phone_number"] == "+3942390027"
    refute Map.has_key?(response, "phone_number_verified")
    assert response["non_standard_claim_1"] == "some value"
  end

  defp simplified_content_type_from_conn(conn) do
    Plug.Conn.get_resp_header(conn, "content-type")
    |> Enum.map(fn
      val ->
        case String.split(val, ";") do
          [_] ->
            val

          [type | _] ->
            type
        end
    end)
  end
end


<|start_filename|>lib/asteroid/object_store/authorization_code.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthorizationCode do
  @moduledoc """
  Behaviour for authorization code store
  """

  @type opts :: Keyword.t()

  @doc """
  Installs the authorization code store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the authorization code store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the authorization code store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an authorization code from its id

  Returns `{:ok, %Asteroid.Token.AuthorizationCode{}}` if the authorization code exists
  and `{:ok, nil}` otherwise.
  """

  @callback get(Asteroid.Token.AuthorizationCode.id(), opts()) ::
              {:ok, Asteroid.Token.AuthorizationCode.t() | nil}
              | {:error, any()}

  @doc """
  Stores an authorization code 

  If the authorization code already exists, all of its data should be erased.
  """

  @callback put(Asteroid.Token.AuthorizationCode.t(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an authorization code
  """

  @callback delete(Asteroid.Token.AuthorizationCode.id(), opts()) :: :ok | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1
end


<|start_filename|>test/test_helper.exs<|end_filename|>
alias Asteroid.{Client, Subject}

ExUnit.start()
# Ecto.Adapters.SQL.Sandbox.mode(Asteroid.Repo, :manual)

Client.gen_new(id: "client_confidential_1")
|> Client.add("client_id", "client_confidential_1")
|> Client.add("client_type", "confidential")
|> Client.add("client_secret", "password1")
|> Client.add("grant_types", [
  "authorization_code",
  "implicit",
  "password",
  "client_credentials",
  "refresh_token",
  "urn:ietf:params:oauth:grant-type:device_code"
])
|> Client.add("response_types", [
  "code",
  "token",
  "id_token",
  "id_token token",
  "code id_token",
  "code token",
  "code id_token token"
])
|> Client.add("scope", [
  "scp1",
  "scp2",
  "scp3",
  "scp4",
  "scp5",
  "scp6",
  "asteroid.introspect",
  "asteroid.register",
  "openid"
])
|> Client.add("redirect_uris", ["https://www.example.com", "https://example.org/auth/web/"])
|> Client.add("resource_server_name", "https://client1.api")
|> Client.add(
  "__asteroid_oauth2_endpoint_register_additional_metadata_fields",
  ["field_1", "field_2", "field_4"]
)
|> Client.store()

Client.gen_new(id: "client_confidential_2")
|> Client.add("client_id", "client_confidential_2")
|> Client.add("client_type", "confidential")
|> Client.add("client_secret", "password2")
|> Client.add("grant_types", ["implicit", "refresh_token"])
|> Client.add("scope", ["scp4", "scp5", "scp6", "scp7", "scp8", "scp9"])
|> Client.add("redirect_uris", ["https://www.example.com"])
|> Client.store()

Client.gen_new(id: "client_confidential_3")
|> Client.add("client_id", "client_confidential_3")
|> Client.add("client_type", "confidential")
|> Client.add("client_secret", "<PASSWORD>")
|> Client.add("grant_types", ["authorization_code"])
|> Client.add("response_types", ["code"])
|> Client.add("redirect_uris", ["https://www.example.com"])
|> Client.add("__asteroid_oauth2_mandatory_pkce_use", true)
|> Client.add("__asteroid_oauth2_endpoint_register_allowed_scopes", ["scp11", "scp12", "scp13"])
|> Client.add("__asteroid_oauth2_endpoint_register_auto_scopes", ["scp17", "scp18", "scp19"])
|> Client.add(
  "__asteroid_oauth2_endpoint_register_default_token_endpoint_auth_method",
  "client_secret_post"
)
|> Client.add(
  "__asteroid_oauth2_endpoint_register_default_grant_types",
  ["authorization_code", "client_credentials", "password"]
)
|> Client.add("scope", ["asteroid.register"])
|> Client.store()

Client.gen_new(id: "client_public_1")
|> Client.add("client_id", "client_public_1")
|> Client.add("client_type", "public")
|> Client.add("scope", ["scp1", "scp2", "scp3", "scp4", "scp5", "scp6"])
|> Client.add("grant_types", [
  "authorization_code",
  "implicit",
  "refresh_token",
  "urn:ietf:params:oauth:grant-type:device_code"
])
|> Client.store()

Client.gen_new(id: "client_public_2")
|> Client.add("client_id", "client_public_2")
|> Client.add("client_type", "public")
|> Client.add("client_secret", "<PASSWORD>")
|> Client.add("scope", ["scp1", "scp2", "scp3", "scp4", "scp7", "scp8"])
|> Client.store()

Subject.gen_new(id: "user_1")
|> Subject.add("sub", "user_1")
|> Subject.add("password", "<PASSWORD>")
|> Subject.add("nickname", "<NAME>")
|> Subject.add("email", "<EMAIL>")
|> Subject.add("phone_number", "+3942390027")
|> Subject.add("non_standard_claim_1", "some value")
|> Subject.store()


<|start_filename|>lib/asteroid/object_store/device_code/riak.ex<|end_filename|>
defmodule Asteroid.ObjectStore.DeviceCode.Riak do
  @moduledoc """
  Riak implementation of the `Asteroid.ObjectStore.DeviceCode` behaviour

  ## Initializing a Riak bucket type

  ```console
  $ sudo riak-admin bucket-type create ephemeral_token '{"props":{"datatype":"map", "backend":"leveldb_mult"}}'
  ephemeral_token created

  $ sudo riak-admin bucket-type activate ephemeral_token
  ephemeral_token has been activated
  ```

  ## Options
  The options (`Asteroid.ObjectStore.DeviceCode.opts()`) are:
  - `:bucket_type`: an `String.t()` for the bucket type that must be created beforehand in
  Riak. No defaults, **mandatory**
  - `bucket_name`: a `String.t()` for the bucket name. Defaults to `"device_code"`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `300` (5 minutes)
  - `:rows`: the maximum number of results that a search will return. Defaults to `1_000_000`.
  Search is used by the purge process.

  ## Installation function

  The `install/1` function executes the following actions:
  - it installs a custom schema (`asteroid_object_store_device_code_riak_schema`)
  - it creates a new index (`asteroid_object_store_device_code_riak_index`) on the bucket
  (and not the bucket type - so as to avoid collisions)

  This is necessary to:
  1. Efficiently index expiration timestamp
  2. Disable indexing of raw device code data

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Riak).
  """

  require Logger

  @behaviour Asteroid.ObjectStore.DeviceCode

  @impl true

  def install(opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "device_code"

    with :ok <-
           Riak.Search.Schema.create(
             schema_name(),
             (:code.priv_dir(:asteroid) ++ '/riak/object_store_device_code_schema.xml')
             |> File.read!()
           ),
         :ok <- Riak.Search.Index.put(index_name(), schema_name()),
         :ok <- Riak.Search.Index.set({bucket_type, bucket_name}, index_name()) do
      Logger.info(
        "#{__MODULE__}: created device code store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}`"
      )

      :ok
    else
      e ->
        "#{__MODULE__}: failed to create device code store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

        {:error, "#{inspect(e)}"}
    end
  catch
    :exit, e ->
      bucket_type = opts[:bucket_type] || raise "Missing bucket type"
      bucket_name = opts[:bucket_name] || "device_code"

      "#{__MODULE__}: failed to create device code store `#{bucket_name}` " <>
        "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

      {:error, "#{inspect(e)}"}
  end

  @impl true

  def start_link(opts) do
    opts = Keyword.merge([purge_interval: 300], opts)

    # we launch the process anyway because we need to return a process
    # but the singleton will do nothing if the value is `:no_purge`
    Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)
  end

  @impl true

  def get(device_code_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "device_code"

    case Riak.find(bucket_type, bucket_name, device_code_id) do
      res when not is_nil(res) ->
        device_code =
          res
          |> Riak.CRDT.Map.get(:register, "device_code_data_binary")
          |> Base.decode64!(padding: false)
          |> :erlang.binary_to_term()

        Logger.debug(
          "#{__MODULE__}: getting device code `#{device_code_id}`, " <>
            "value: `#{inspect(device_code)}`"
        )

        {:ok, device_code}

      nil ->
        Logger.debug("#{__MODULE__}: getting device code `#{device_code_id}`, " <> "value: `nil`")

        {:ok, nil}
    end
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def get_from_user_code(user_code, opts) do
    query = "user_code:\"#{String.replace(user_code, "\"", "\\\"")}\""

    case search(query, opts) do
      {:ok, [device_code_id]} ->
        get(device_code_id, opts)

      {:ok, [_ | _]} ->
        {:error, "Duplicate user code in the device authorization flow"}

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def put(device_code, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "device_code"

    riak_map = Riak.CRDT.Map.new()

    device_code_data_binary =
      device_code
      |> :erlang.term_to_binary()
      |> Base.encode64(padding: false)
      |> Riak.CRDT.Register.new()

    riak_map = Riak.CRDT.Map.put(riak_map, "device_code_data_binary", device_code_data_binary)

    riak_map =
      Riak.CRDT.Map.put(
        riak_map,
        "user_code",
        Riak.CRDT.Register.new(device_code.user_code)
      )

    riak_map =
      if device_code.data["exp"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "exp_int",
          Riak.CRDT.Register.new(to_string(device_code.data["exp"]))
        )
      else
        Logger.warn(
          "Inserting device code with no expiration: #{String.slice(device_code.id, 1..5)}..."
        )

        riak_map
      end

    Riak.update(riak_map, bucket_type, bucket_name, device_code.id)

    Logger.debug(
      "#{__MODULE__}: stored device code `#{device_code.id}`, " <>
        "value: `#{inspect(device_code)}`"
    )

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def delete(device_code_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "device_code"

    Riak.delete(bucket_type, bucket_name, device_code_id)

    Logger.debug("#{__MODULE__}: deleted device code `#{device_code_id}`")

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @doc """
  Searches in Riak-stored device code

  This function is used internaly and made available for user convenience. Device codes are
  stored in the following fields:

  |               Field name          |  Indexed as   |
  |-----------------------------------|:-------------:|
  | device_code_data_binary_register  | *not indexed* |
  | user_code_register                | string        |
  | exp_int_register                  | int           |

  Note that you are responsible for escaping values accordingly with Solr escaping.
  """

  @spec search(String.t(), Asteroid.ObjectStore.DeviceCode.opts()) ::
          {:ok, [Asteroid.OAuth2.DeviceAuthorization.device_code()]}
          | {:error, any()}

  def search(search_query, opts) do
    case Riak.Search.query(index_name(), search_query, rows: opts[:rows] || 1_000_000) do
      {:ok, {:search_results, result_list, _, _}} ->
        {:ok,
         for {_index_name, attribute_list} <- result_list do
           :proplists.get_value("_yz_rk", attribute_list)
         end}

      {:error, _} = error ->
        error
    end
  end

  @spec schema_name() :: String.t()

  defp schema_name(), do: "asteroid_object_store_device_code_riak_schema"

  @doc false

  @spec index_name() :: String.t()

  def index_name(), do: "asteroid_object_store_device_code_riak_index"
end


<|start_filename|>lib/asteroid/object_store/access_token/riak/purge.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AccessToken.Riak.Purge do
  @moduledoc false

  use GenServer
  require Logger

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def init(opts) do
    if opts[:purge_interval] != :no_purge do
      Process.send_after(self(), :purge, opts[:purge_interval] * 1000)
    end

    {:ok, opts}
  end

  def handle_info(:purge, opts) do
    purge(opts)

    Process.send_after(self(), :purge, opts[:purge_interval] * 1000)

    {:noreply, opts}
  end

  defp purge(opts) do
    Logger.info("#{__MODULE__}: starting access token purge process on #{node()}")

    request = "exp_int_register:[0 TO #{:os.system_time(:second)}]"

    case Asteroid.ObjectStore.AccessToken.Riak.search(request, opts) do
      {:ok, access_token_ids} ->
        for access_token_id <- access_token_ids do
          # this causes Riak connection exhaustion, to investigate further
          # Task.start(Asteroid.ObjectStore.AccessToken.Riak, :delete, [access_token_id, opts])
          Asteroid.Token.AccessToken.delete(access_token_id)
        end

        :ok

      {:error, _} = error ->
        Logger.warn(
          "#{__MODULE__}: purge process on #{node()} failed with error #{inspect(error)}"
        )
    end
  end
end


<|start_filename|>custom_example/callback.ex<|end_filename|>
defmodule CustomExample.Callback do
  @moduledoc false

  import Asteroid.Utils

  alias Asteroid.OIDC
  alias Asteroid.Token.AccessToken
  alias Asteroid.Subject

  def add_dad_joke(access_token, %{flow: :client_credentials}) do
    response = HTTPoison.get!("https://icanhazdadjoke.com/", [{"Accept", "text/plain"}])

    AccessToken.put_value(access_token, "dad_joke", response.body)
  end

  def add_dad_joke(access_token, _ctx) do
    access_token
  end

  def introspect_add_subject_attributes(response, %{subject: subject}) do
    subject = Subject.fetch_attributes(subject, ["mail", "permissions"])

    response
    |> put_if_not_nil("email_address", subject.attrs["mail"])
    |> put_if_not_nil("permissions", subject.attrs["permissions"])
  end

  def introspect_add_subject_attributes(response, _) do
    response
  end

  def introspect_add_authenticated_session_info(response, %{token: token}) do
    case token.data["__asteroid_oidc_authenticated_session_id"] do
      session_id when is_binary(session_id) ->
        session_info = OIDC.AuthenticatedSession.info(session_id) || %{}

        response
        |> put_if_not_nil("current_acr", session_info[:acr])
        |> put_if_not_nil("current_amr", session_info[:amr])
        |> put_if_not_nil("current_auth_time", session_info[:auth_time])

      nil ->
        response
    end
  end

  @spec test_ropc_username_password_callback(Plug.Conn.t(), String.t(), String.t())
    :: {:ok, Asteroid.Subject.t()} | {:error, atom()}

  def test_ropc_username_password_callback(_conn, username, password) do
    case Subject.load(username, attributes: ["password"]) do
      {:ok, sub} ->
        if sub.attrs["password"] == password do
          {:ok, sub}
        else
          {:error, Asteroid.OAuth2.InvalidGrantError.exception(
            grant: "password",
            reason: "invalid username or password",
            debug_details: "passwords don't match"
          )}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end
end


<|start_filename|>lib/asteroid/oauth2/scope.ex<|end_filename|>
defmodule Asteroid.OAuth2.Scope do
  @moduledoc """
  Scope helper functions and default callbacks
  """

  alias OAuth2Utils.Scope
  alias Asteroid.Context
  alias Asteroid.OAuth2

  import Asteroid.Utils

  defmodule UnknownRequestedScopeError do
    @moduledoc """
    Error return when an unknown scope has been reuqested
    """

    defexception [:unknown_scopes]

    @type t :: %__MODULE__{
            unknown_scopes: Scope.Set.t()
          }

    def message(%{unknown_scopes: unknown_scopes}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Unknown requested scope(s)" <>
            if unknown_scopes do
              " (#{Enum.join(unknown_scopes, " ")})"
            else
              ""
            end

        :normal ->
          "Unknown requested scope(s)"

        :minimal ->
          ""
      end
    end
  end

  @typedoc """
  Individual scope configuration keys

  The processing rules are:
  - `:auto`: the scope will automatically be granted, even when not requested
  - `:advertise`: determine whether the scope is advertised on the `/.well-nown` URIs. Defaults
  to `true`. If set in a incoherent way within different flows, the behaviour is unspecified.
  - `:display`: in *web flows*, display that scope to the end-user for authorization. When
  not present, shall be treated as `true`
  - `:optional`: in *web flows*, make that scope optional, so that the user can deselect it even
  when this was requested by the client. When not present, shall be treated as `false`
  - `:label`: a map of internationalised labels of the scope, that will be displayed to the
  end-user. The map keys are ISO639 tags, and the values the internationalised text of the label
  - `:acceptable_acrs`: a list of ACRs for use in OIDC flows. When present, Asteroid will set
  the preferred ACR in the `t:AsteroiWeb.AuthorizeController.Request.t/0` to a value that
  satisfies this requirement if the scope is requested. It will return an error otherwise
  - `:max_refresh_token_lifetime`: *when present*, restricts the lifetime of a refresh token
  released when that scope is granted. This *supersedes global*, flow or client refresh token
  lifetime configuration
  - `:max_access_token_lifetime`: *when present*, restricts the lifetime of an access token
  released when that scope is granted. This *supersedes global*, flow or client acess token
  lifetime configuration
  """

  @type scope_config_option_individual_scope_configuration ::
          {:auto, boolean()}
          | {:display, boolean()}
          | {:optional, boolean()}
          | {:label, %{required(String.t()) => String.t()}}
          | {:acceptable_acrs, [Asteroid.OIDC.acr()]}
          | {:max_refresh_token_lifetime, non_neg_integer()}
          | {:max_access_token_lifetime, non_neg_integer()}

  @typedoc """
  Scope configuration option type
  """

  @type scope_config_option ::
          [
            {:scopes,
             %{required(String.t()) => [scope_config_option_individual_scope_configuration()]}}
          ]

  @doc """
  Returns the merged scope configuration for a flow

  Scope configuration is merge at the key level of a individual scope configuration.
  """

  @spec configuration_for_flow(OAuth2.flow()) :: scope_config_option()

  def configuration_for_flow(flow)
      when flow in [
             :ropc,
             :client_credentials,
             :authorization_code,
             :implicit,
             :device_authorization,
             :oidc_authorization_code,
             :oidc_implicit,
             :oidc_hybrid
           ] do
    scope_config = astrenv(:scope_config)
    oauth2_scope_config = astrenv(:oauth2_scope_config)

    oauth2_flow_scope_config =
      case flow do
        :ropc ->
          astrenv(:oauth2_flow_ropc_scope_config, [])

        :client_credentials ->
          astrenv(:oauth2_flow_client_credentials_scope_config, [])

        :authorization_code ->
          astrenv(:oauth2_flow_authorization_code_scope_config, [])

        :implicit ->
          astrenv(:oauth2_flow_implicit_scope_config, [])

        :device_authorization ->
          astrenv(:oauth2_flow_device_authorization_scope_config, [])

        :oidc_authorization_code ->
          astrenv(:oidc_flow_authorization_code_scope_config, [])

        :oidc_implicit ->
          astrenv(:oidc_flow_implicit_scope_config, [])

        :oidc_hybrid ->
          astrenv(:oidc_flow_hybrid_scope_config, [])
      end

    merged_individual_scope_config =
      Enum.reduce(
        [scope_config, oauth2_scope_config, oauth2_flow_scope_config],
        %{},
        fn
          conf, acc ->
            individual_scope_config = conf[:scopes] || %{}

            Map.merge(acc, individual_scope_config)
        end
      )

    [scopes: merged_individual_scope_config]
  end

  @doc """
  Given a set of scopes and a `t:scope_config_option/0`, returns the max refresh token lifetime
  or `nil` if not present
  """

  @spec max_refresh_token_lifetime(Scope.Set.t(), scope_config_option()) ::
          non_neg_integer()
          | nil

  def max_refresh_token_lifetime(scopes, scope_config_option) do
    Enum.reduce(
      scopes,
      [],
      fn
        scope, acc ->
          case scope_config_option[:scopes][scope][:max_refresh_token_lifetime] do
            lifetime when is_integer(lifetime) ->
              acc ++ [lifetime]

            nil ->
              acc
          end
      end
    )
    |> Enum.max(fn -> nil end)
  end

  @doc """
  Given a set of scopes and a `t:scope_config_option/0`, returns the max access token lifetime
  or `nil` if not present
  """

  @spec max_access_token_lifetime(Scope.Set.t(), scope_config_option()) ::
          non_neg_integer()
          | nil

  def max_access_token_lifetime(scopes, scope_config_option) do
    Enum.reduce(
      scopes,
      [],
      fn
        scope, acc ->
          case scope_config_option[:scopes][scope][:max_access_token_lifetime] do
            lifetime when is_integer(lifetime) ->
              acc ++ [lifetime]

            nil ->
              acc
          end
      end
    )
    |> Enum.max(fn -> nil end)
  end

  @doc """
  Returns the scopes available to a flow
  """

  @spec scopes_for_flow(OAuth2.flow()) :: Scope.Set.t()

  def scopes_for_flow(flow)
      when flow in [
             :ropc,
             :client_credentials,
             :authorization_code,
             :implicit,
             :device_authorization,
             :oidc_authorization_code,
             :oidc_implicit,
             :oidc_hybrid
           ] do
    Enum.reduce(
      configuration_for_flow(flow)[:scopes] || %{},
      Scope.Set.new(),
      fn
        {scope, _}, acc ->
          Scope.Set.put(acc, scope)
      end
    )
  end

  @doc """
  Returns `:ok` if the scopes are enabled for the given flow, false otherwise
  """

  @spec scopes_enabled?(Scope.Set.t(), OAuth2.flow()) ::
          :ok
          | {:error, %UnknownRequestedScopeError{}}

  def scopes_enabled?(scopes, flow) do
    enabled_scopes_for_flow = scopes_for_flow(flow)

    if Scope.Set.subset?(scopes, enabled_scopes_for_flow) do
      :ok
    else
      {:error,
       UnknownRequestedScopeError.exception(
         unknown_scopes: Scope.Set.difference(scopes, enabled_scopes_for_flow)
       )}
    end
  end

  @doc """
  Computes scopes to grant during requests

  Note that the list of scopes allowed for a client is directly configured in the client's
  attribute repository.

  ## ROPC flow

  The functions adds the scopes marked as `auto: true` in accordance to the
  #{Asteroid.Config.link_to_option(:oauth2_flow_ropc_scope_config)}
  configuration option, only during the initial request (when the username and password
  parameters are provided).

  On further token renewal requests the released scopes are the ones requested  and already
  granted during the initial request, or a subset of them.

  ## Client credentials flow

  The functions adds the scopes marked as `auto: true` in accordance to the
  #{Asteroid.Config.link_to_option(:oauth2_flow_client_credentials_scope_config)}
  configuration option, only during the initial request.

  On further token renewal requests the released scopes are the ones requested and already
  granted during the initial request, or a subset of them, although you should probably not
  use refresh tokens in such a flow.

  ## Authorization code flow

  The functions adds the scopes marked as `auto: true` in accordance to the
  #{Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_scope_config)}
  configuration option when the web flow on the `/authorize` endpoint successfully concludes.

  ## Implicit flow

  The functions adds the scopes marked as `auto: true` in accordance to the
  #{Asteroid.Config.link_to_option(:oauth2_flow_implicit_scope_config)}
  configuration option when the web flow on the `/authorize` endpoint successfully concludes.

  ## Device authorization flow
  During the initial phase of the flow, when the client requests a device code on the
  `/api/oauth2/device_authorization` endpoint, this function does not change the scopes.

  The functions adds the scopes marked as `auto: true` in accordance to the
  #{Asteroid.Config.link_to_option(:oauth2_flow_device_authorization_scope_config)}
  configuration option when the web flow on the `/device` endpoint successfully concludes.
  """

  @spec grant_for_flow(Scope.Set.t(), Context.t()) :: Scope.Set.t()

  def grant_for_flow(scopes, %{flow: :ropc, grant_type: :password}) do
    Enum.reduce(
      astrenv(:oauth2_flow_ropc_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{flow: :client_credentials, grant_type: :client_credentials}) do
    Enum.reduce(
      astrenv(:oauth2_flow_client_credentials_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{endpoint: :authorize, flow: :authorization_code}) do
    Enum.reduce(
      astrenv(:oauth2_flow_authorization_code_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{endpoint: :authorize, flow: :implicit}) do
    Enum.reduce(
      astrenv(:oauth2_flow_implicit_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{flow: :device_authorization, endpoint: :device_authorization}) do
    scopes
  end

  def grant_for_flow(scopes, %{flow: :device_authorization, endpoint: :device}) do
    Enum.reduce(
      astrenv(:oauth2_flow_device_authorization_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{endpoint: :authorize, flow: :oidc_authorization_code}) do
    Enum.reduce(
      astrenv(:oidc_flow_authorization_code_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{endpoint: :authorize, flow: :oidc_implicit}) do
    Enum.reduce(
      astrenv(:oidc_flow_implicit_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end

  def grant_for_flow(scopes, %{endpoint: :authorize, flow: :oidc_hybrid}) do
    Enum.reduce(
      astrenv(:oidc_flow_hybrid_scope_config) || [],
      scopes,
      fn
        {scope, scope_config}, acc ->
          if scope_config[:auto] do
            Scope.Set.put(acc, scope)
          else
            acc
          end
      end
    )
  end
end


<|start_filename|>lib/asteroid/object_store/authenticated_session/riak/purge.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticatedSession.Riak.Purge do
  @moduledoc false

  use GenServer
  require Logger

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def init(opts) do
    if opts[:purge_interval] != :no_purge do
      Process.send_after(self(), :purge, opts[:purge_interval] * 1000)
    end

    {:ok, opts}
  end

  def handle_info(:purge, opts) do
    purge(opts)

    Process.send_after(self(), :purge, opts[:purge_interval] * 1000)

    {:noreply, opts}
  end

  defp purge(opts) do
    Logger.info("#{__MODULE__}: starting authenticated session purge process on #{node()}")

    request = "exp_int_register:[0 TO #{:os.system_time(:second)}]"

    case Asteroid.ObjectStore.AuthenticatedSession.Riak.search(request, opts) do
      {:ok, authenticated_session_ids} ->
        for authenticated_session_id <- authenticated_session_ids do
          # this causes Riak connection exhaustion, to investigate further
          # Task.start(Asteroid.ObjectStore.AuthenticatedSession.Riak, :delete, [authenticated_session_id, opts])
          Asteroid.OIDC.AuthenticatedSession.delete(authenticated_session_id)
        end

        :ok

      {:error, _} = error ->
        Logger.warn(
          "#{__MODULE__}: purge process on #{node()} failed with error #{inspect(error)}"
        )
    end
  end
end


<|start_filename|>lib/asteroid_web/controllers/api/oauth2/introspect_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.IntrospectController do
  @moduledoc false

  use AsteroidWeb, :controller
  import Asteroid.Utils
  alias Asteroid.Token.{RefreshToken, AccessToken}
  alias Asteroid.{Client, Subject}
  alias Asteroid.OAuth2

  def handle(%Plug.Conn{body_params: %{"token" => token}} = conn, params) do
    with {:ok, client} <- OAuth2.Client.get_authenticated_client(conn),
         :ok <- valid_token_parameter?(token),
         :ok <- astrenv(:oauth2_endpoint_introspect_client_authorized).(client) do
      do_handle(conn, params, client)
    else
      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, _) do
    error_resp(conn, 400,
      error: :invalid_request,
      error_description: "Missing `token` parameter"
    )
  end

  def do_handle(conn, %{"token" => token} = params, client) do
    case params["token_type_hint"] do
      token_type_hint when token_type_hint in [nil, "access_token"] ->
        case AccessToken.get(token) do
          {:ok, access_token} ->
            introspect_access_token(conn, access_token, client)

          {:error, _} ->
            case RefreshToken.get(token) do
              {:ok, refresh_token} ->
                introspect_refresh_token(conn, refresh_token, client)

              {:error, _} ->
                token_not_found_resp(conn, client)
            end
        end

      "refresh_token" ->
        case RefreshToken.get(token) do
          {:ok, refresh_token} ->
            introspect_refresh_token(conn, refresh_token, client)

          {:error, _} ->
            case AccessToken.get(token) do
              {:ok, refresh_token} ->
                introspect_access_token(conn, refresh_token, client)

              {:error, _} ->
                token_not_found_resp(conn, client)
            end
        end

      _ ->
        error_resp(conn, 400,
          error: :invalid_request,
          error_description: "Unrecognized `token_type_hint`"
        )
    end
  end

  @spec introspect_access_token(Plug.Conn.t(), AccessToken.t(), Client.t()) :: Plug.Conn.t()

  defp introspect_access_token(conn, access_token, client) do
    maybe_subject =
      case Subject.load_from_unique_attribute("sub", access_token.data["sub"]) do
        {:ok, subject} ->
          subject

        _ ->
          nil
      end

    ctx =
      %{}
      |> Map.put(:endpoint, :introspect)
      |> put_if_not_nil(:subject, maybe_subject)
      |> Map.put(:client, client)
      |> Map.put(:token, access_token)
      |> Map.put(:token_sort, :access_token)
      |> Map.put(:conn, conn)

    response_claims = astrenv(:oauth2_endpoint_introspect_claims_resp_callback).(ctx)

    resp =
      access_token.data
      |> Enum.filter(fn {k, _} -> k in response_claims end)
      |> Enum.into(%{})
      |> scope_list_to_param()
      |> Map.put("active", true)
      |> astrenv(:oauth2_endpoint_introspect_before_send_resp_callback).(ctx)

    conn
    |> put_status(200)
    |> astrenv(:oauth2_endpoint_introspect_before_send_conn_callback).(ctx)
    |> json(resp)
  end

  @spec introspect_refresh_token(Plug.Conn.t(), RefreshToken.t(), Client.t()) :: Plug.Conn.t()
  defp introspect_refresh_token(conn, refresh_token, client) do
    maybe_subject =
      case Subject.load_from_unique_attribute("sub", refresh_token.data["sub"]) do
        {:ok, subject} ->
          subject

        _ ->
          nil
      end

    ctx =
      %{}
      |> Map.put(:endpoint, :introspect)
      |> put_if_not_nil(:subject, maybe_subject)
      |> Map.put(:client, client)
      |> Map.put(:token, refresh_token)
      |> Map.put(:token_sort, :refresh_token)
      |> Map.put(:conn, conn)

    response_claims = astrenv(:oauth2_endpoint_introspect_claims_resp_callback).(ctx)

    resp =
      refresh_token.data
      |> Enum.filter(fn {k, _} -> k in response_claims end)
      # since Enum.filter/2 returns a list
      |> Enum.into(%{})
      |> scope_list_to_param()
      |> Map.put("active", true)
      |> astrenv(:oauth2_endpoint_introspect_before_send_resp_callback).(ctx)

    conn
    |> put_status(200)
    |> astrenv(:oauth2_endpoint_introspect_before_send_conn_callback).(ctx)
    |> json(resp)
  end

  @spec valid_token_parameter?(String.t()) ::
          :ok
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp valid_token_parameter?(token) do
    if OAuth2Utils.valid_access_token_param?(token) or
         OAuth2Utils.valid_refresh_token_param?(token) do
      :ok
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(
         name: "token",
         value: token
       )}
    end
  end

  @spec token_not_found_resp(Plug.Conn.t(), Client.t()) :: Plug.Conn.t()

  defp token_not_found_resp(conn, client) do
    ctx =
      %{}
      |> Map.put(:endpoint, :introspect)
      |> Map.put(:client, client)
      |> Map.put(:conn, conn)

    resp =
      %{"active" => false}
      |> astrenv(:oauth2_endpoint_introspect_before_send_resp_callback).(ctx)

    conn
    |> put_status(200)
    |> astrenv(:oauth2_endpoint_introspect_before_send_conn_callback).(ctx)
    |> json(resp)
  end

  defp error_resp(conn, error_status, error_data) do
    conn
    |> put_status(error_status)
    |> json(Enum.into(error_data, %{}))
  end

  @spec scope_list_to_param(map()) :: map()

  defp scope_list_to_param(%{"scope" => []} = m) do
    Map.delete(m, "scope")
  end

  defp scope_list_to_param(%{"scope" => scopes} = m) when is_list(scopes) do
    Map.put(m, "scope", Enum.join(scopes, " "))
  end

  defp scope_list_to_param(m) do
    m
  end
end


<|start_filename|>lib/asteroid/oauth2/introspect.ex<|end_filename|>
defmodule Asteroid.OAuth2.Introspect do
  alias Asteroid.Context
  alias Asteroid.Client

  import Asteroid.Utils

  @doc """
  Callback invoked to determine which claims to return from the `"/introspect"` endpoint

  ## Processing rules
  - If the `"__asteroid_endpoint_introspect_claims_resp"` attribute of the client is set to
  a list of `String.t()` claims, returns it
  - Otherwise, if the `:oauth2_endpoint_introspect_claims_resp` is set to a list of `String.t()`
  clains, returns it
  - Otherwise returns an empty list `[]`
  """

  @spec endpoint_introspect_claims_resp(Context.t()) :: [String.t()]

  def endpoint_introspect_claims_resp(%{client: client}) do
    client = Client.fetch_attributes(client, ["__asteroid_endpoint_introspect_claims_resp"])

    if is_list(client.attrs["__asteroid_endpoint_introspect_claims_resp"]) do
      client.attrs["__asteroid_endpoint_introspect_claims_resp"]
    else
      endpoint_introspect_claims_resp(%{})
    end
  end

  def endpoint_introspect_claims_resp(_) do
    astrenv(:oauth2_endpoint_introspect_claims_resp, [])
  end
end


<|start_filename|>test/asteroid_web/views/layout_view_test.exs<|end_filename|>
defmodule AsteroidWeb.LayoutViewTest do
  use AsteroidWeb.ConnCase, async: true
end


<|start_filename|>lib/asteroid_web/router.ex<|end_filename|>
defmodule AsteroidWeb.Router do
  @moduledoc false

  use AsteroidWeb, :router
  use Plug.ErrorHandler

  import Asteroid.Utils

  alias Asteroid.OAuth2

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers

    for {plug_module, plug_options} <- astrenv(:browser_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :well_known do
    for {plug_module, plug_options} <- astrenv(:well_known_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :discovery do
    for {plug_module, plug_options} <- astrenv(:discovery_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :api_urlencoded do
    plug :accepts, ["json"]
    plug Plug.Parsers, parsers: [:urlencoded]
  end

  pipeline :api_json do
    plug :accepts, ["json"]
    plug Plug.Parsers, parsers: [:json], json_decoder: Jason
  end

  pipeline :request_object do
    for {plug_module, plug_options} <- astrenv(:api_request_object_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oidc do
    for {plug_module, plug_options} <- astrenv(:api_oidc_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oidc_endpoint_userinfo do
    for {plug_module, plug_options} <- astrenv(:api_oidc_endpoint_userinfo_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oauth2 do
    for {plug_module, plug_options} <- astrenv(:api_oauth2_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oauth2_endpoint_token do
    for {plug_module, plug_options} <- astrenv(:api_oauth2_endpoint_token_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oauth2_endpoint_introspect do
    for {plug_module, plug_options} <- astrenv(:api_oauth2_endpoint_introspect_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oauth2_endpoint_revoke do
    for {plug_module, plug_options} <- astrenv(:api_oauth2_endpoint_revoke_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oauth2_endpoint_register do
    for {plug_module, plug_options} <- astrenv(:api_oauth2_endpoint_register_plugs, []) do
      plug plug_module, plug_options
    end
  end

  pipeline :oauth2_endpoint_device_authorization do
    for {plug_module, plug_options} <-
          astrenv(:api_oauth2_endpoint_device_authorization_plugs, []) do
      plug plug_module, plug_options
    end
  end

  scope "/", AsteroidWeb do
    pipe_through :browser

    get "/authorize", AuthorizeController, :pre_authorize
    get "/device", DeviceController, :pre_authorize
  end

  scope "/api", AsteroidWeb.API do
    scope "/request_object" do
      pipe_through :request_object

      get "/:id", RequestObjectController, :show
      post "/", RequestObjectController, :create
    end
  end

  scope "/api/oidc", AsteroidWeb.API.OIDC do
    pipe_through :oidc

    scope "/userinfo" do
      pipe_through :api_urlencoded
      pipe_through :oidc_endpoint_userinfo

      get "/", UserinfoController, :show
      post "/", UserinfoController, :show
    end
  end

  scope "/api/oauth2", AsteroidWeb.API.OAuth2 do
    pipe_through :oauth2

    scope "/token" do
      pipe_through :api_urlencoded
      pipe_through :oauth2_endpoint_token

      post "/", TokenController, :handle
    end

    scope "/introspect" do
      pipe_through :api_urlencoded
      pipe_through :oauth2_endpoint_introspect

      post "/", IntrospectController, :handle
    end

    scope "/revoke" do
      pipe_through :api_urlencoded
      pipe_through :oauth2_endpoint_revoke

      post "/", RevokeController, :handle
    end

    scope "/register" do
      pipe_through :api_json
      pipe_through :oauth2_endpoint_register

      post "/", RegisterController, :handle
    end

    scope "/device_authorization" do
      pipe_through :api_json
      pipe_through :oauth2_endpoint_device_authorization

      post "/", DeviceAuthorizationController, :handle
    end
  end

  scope "/.well-known", AsteroidWeb.WellKnown do
    pipe_through :well_known

    get "/oauth-authorization-server", OauthAuthorizationServerController, :handle
    get "/openid-configuration", OauthAuthorizationServerController, :handle
  end

  scope "/discovery", AsteroidWeb.Discovery do
    pipe_through :discovery

    get "/keys", KeysController, :handle
  end

  def handle_errors(conn, %{kind: _kind, reason: reason, stack: stack}) do
    conn
    |> AsteroidWeb.Error.respond_api(
      OAuth2.ServerError.exception(
        reason: inspect(reason),
        stacktrace: stack
      )
    )
  end
end


<|start_filename|>config/dev.exs<|end_filename|>
use Mix.Config

# For development, we disable any cache and enable
# debugging and code reloading.
#
# The watchers configuration can be used to run external
# watchers to your application. For example, we use it
# with webpack to recompile .js and .css sources.
#
config :asteroid, AsteroidWeb.Endpoint,
  http: [port: 4000],
  # url: [scheme: "https", host: "www.example.com", path: "/account/auth", port: 443],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  watchers: [
    node: [
      "node_modules/webpack/bin/webpack.js",
      "--mode",
      "development",
      "--watch-stdin",
      cd: Path.expand("../assets", __DIR__)
    ]
  ]

# ## SSL Support
#
# In order to use HTTPS in development, a self-signed
# certificate can be generated by running the following
# Mix task:
#
#     mix phx.gen.cert
#
# Note that this task requires Erlang/OTP 20 or later.
# Run `mix help phx.gen.cert` for more information.
#
# The `http:` config above can be replaced with:
#
#     https: [
#       port: 4001,
#       cipher_suite: :strong,
#       keyfile: "priv/cert/selfsigned_key.pem",
#       certfile: "priv/cert/selfsigned.pem"
#     ],
#
# If desired, both `http:` and `https:` keys can be
# configured to run both http and https servers on
# different ports.

# Watch static and templates for browser reloading.
config :asteroid, AsteroidWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
      ~r{priv/gettext/.*(po)$},
      ~r{lib/asteroid_web/views/.*(ex)$},
      ~r{lib/asteroid_web/templates/.*(eex)$}
    ]
  ]

# Do not include metadata nor timestamps in development logs
config :logger, :console, format: "[$level] $message\n"

# Set a higher stacktrace during development. Avoid configuring such
# in production as building large stacktraces may be expensive.
config :phoenix, :stacktrace_depth, 20

# Initialize plugs at runtime for faster development compilation
config :phoenix, :plug_init_mode, :runtime

config :mnesia,
  dir: 'Mnesia.#{node()}-#{Mix.env()}'

# Hammer is used for cache in some plugs (rate-limiting) and for the OAuth2 device flow

config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}

# to use Riak uncomment and configure the following lines

# config :pooler,
#  pools: [
#    [
#      name: :riak,
#      group: :riak,
#      max_count: 10,
#      init_count: 5,
#      start_mfa: {Riak.Connection, :start_link, ['127.0.0.1', 8087]}
#    ]
#  ]

######################################################################
######################################################################
################## Asteroid configuration ############################
######################################################################
######################################################################

####################### Token stores #################################

config :asteroid, :object_store_access_token, module: Asteroid.ObjectStore.AccessToken.Mnesia

config :asteroid, :object_store_refresh_token, module: Asteroid.ObjectStore.RefreshToken.Mnesia

config :asteroid, :object_store_authorization_code,
  module: Asteroid.ObjectStore.AuthorizationCode.Mnesia

config :asteroid, :object_store_device_code, module: Asteroid.ObjectStore.DeviceCode.Mnesia

config :asteroid, :object_store_authenticated_session,
  module: Asteroid.ObjectStore.AuthenticatedSession.Mnesia

config :asteroid, :object_store_authentication_event,
  module: Asteroid.ObjectStore.AuthenticationEvent.Mnesia

config :asteroid, :object_store_request_object,
  module: Asteroid.ObjectStore.GenericKV.Mnesia,
  opts: [table_name: :request_object]

config :asteroid,
       :object_store_refresh_token_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_access_token_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authorization_code_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authenticated_session_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authentication_event_before_store_callback,
       &Asteroid.Utils.id_first_param/2

####################### Attribute repositories #######################

# defaults to Mnesia in-memory backend
#
# To enable persistence, add the current node (or another) to the `disc_copies` mnesia option:
#
#   init_opts: [instance: :client, mnesia_config: [disc_copies: [node()]]],
#
# However, before doing that, make sure you understand that it stores sensitive information on
# the disc, such as personal user informations and application passwords, which must be properly
# protected.

config :asteroid, :attribute_repositories,
  subject: [
    module: AttributeRepositoryMnesia,
    init_opts: [instance: :subject],
    run_opts: [instance: :subject]
  ],
  client: [
    module: AttributeRepositoryMnesia,
    init_opts: [instance: :client],
    run_opts: [instance: :client]
  ],
  device: [
    module: AttributeRepositoryMnesia,
    init_opts: [instance: :device],
    run_opts: [instance: :device]
  ]

####################### API plugs ###################################

config :asteroid, :browser_plugs, []

config :asteroid, :api_oauth2_plugs, [
  {
    APIacAuthBasic,
    # ,
    realm: "Asteroid",
    callback: &Asteroid.OAuth2.Client.get_client_secret/2,
    set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
    error_response_verbosity: :debug
  }
  # uncomment the above `#` and the following lines to enable client_secret_post client
  # authentication for all OAuth2 endpoints
  # {APIacAuthClientSecretPost,
  #  realm: "Asteroid",
  #  callback: &Asteroid.Utils.always_nil/2,
  #  set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
  #  error_response_verbosity: :debug}
]

config :asteroid, :api_oauth2_endpoint_token_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug}
  # uncomment the following lines to enable CORS on the /api/oauth2/token endpoint
  # {Corsica, [origins: "*"]},
  # uncomment the following line to enable throttling for public clients on the
  # /api/oauth2/token endpoint
  # {APIacFilterThrottler,
  #  key: &APIacFilterThrottler.Functions.throttle_by_ip_path/1,
  #  scale: 60_000,
  #  limit: 50,
  #  exec_cond: &Asteroid.Utils.conn_not_authenticated?/1,
  #  error_response_verbosity: :debug},
]

config :asteroid, :api_oauth2_endpoint_introspect_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug}
]

config :asteroid, :api_oauth2_endpoint_revoke_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug}
  # uncomment the following lines to enable CORS on the /api/oauth2/token endpoint
  # {Corsica, [origins: "*"]}
]

config :asteroid, :api_oauth2_endpoint_register_plugs, []

config :asteroid, :api_oauth2_endpoint_device_authorization_plugs, []

config :asteroid, :api_oidc_plugs, []

config :asteroid, :api_oidc_endpoint_userinfo_plugs, [
  {Corsica, [origins: "*"]},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {Asteroid.OAuth2.APIacAuthBearer.Validator, []},
   bearer_extract_methods: [:header, :body],
   forward_bearer: true,
   error_response_verbosity: :normal}
]

config :asteroid, :api_request_object_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.send_error_response/3,
   error_response_verbosity: :debug}
]

config :asteroid, :discovery_plugs, []

config :asteroid, :well_known_plugs, []

####################### Crypto configuration #########################

config :asteroid, :crypto_keys, %{
  "key_auto" => {:auto_gen, [params: {:rsa, 2048}, use: :sig, advertise: false]}
}

config :asteroid, :crypto_keys_cache, {Asteroid.Crypto.Key.Cache.ETS, []}

####################### OAuth2 general configuration ################

config :asteroid, :oauth2_grant_types_enabled, [
  :authorization_code,
  :implicit,
  :password,
  :client_credentials,
  :refresh_token,
  :"urn:ietf:params:oauth:grant-type:device_code"
]

config :asteroid, :oauth2_response_types_enabled, [
  :code,
  :token
]

config :asteroid, :oauth2_response_mode_policy, :oidc_only

config :asteroid, :api_error_response_verbosity, :normal

config :asteroid, :oauth2_scope_callback, &Asteroid.OAuth2.Scope.grant_for_flow/2

config :asteroid, :oauth2_access_token_lifetime, 60 * 10

config :asteroid, :oauth2_access_token_lifetime_callback, &Asteroid.Token.AccessToken.lifetime/1

config :asteroid, :oauth2_refresh_token_lifetime, 60 * 60

config :asteroid, :oauth2_refresh_token_lifetime_callback, &Asteroid.Token.RefreshToken.lifetime/1

config :asteroid, :oauth2_authorization_code_lifetime, 60

config :asteroid,
       :oauth2_authorization_code_lifetime_callback,
       &Asteroid.Token.AuthorizationCode.lifetime/1

config :asteroid, :oauth2_issue_refresh_token_init, true

config :asteroid, :oauth2_issue_refresh_token_init, false

config :asteroid,
       :oauth2_issue_refresh_token_callback,
       &Asteroid.Token.RefreshToken.issue_refresh_token?/1

config :asteroid,
       :oauth2_access_token_serialization_format_callback,
       &Asteroid.Token.AccessToken.serialization_format/1

config :asteroid,
       :oauth2_access_token_signing_key_callback,
       &Asteroid.Token.AccessToken.signing_key/1

config :asteroid,
       :oauth2_access_token_signing_alg_callback,
       &Asteroid.Token.AccessToken.signing_alg/1

####################### OAuth2 grant types ###########################

# ROPC

config :asteroid,
       :oauth2_endpoint_token_grant_type_password_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_password_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# refresh token

config :asteroid,
       :oauth2_endpoint_token_grant_type_refresh_token_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_refresh_token_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

####################### OAuth2 endpoints #############################

# authorize

config :asteroid,
       :oauth2_endpoint_authorize_before_send_redirect_uri_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_authorize_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :web_authorization_callback, &Asteroid.WebFlow.web_authorization_callback/2

# token

config :asteroid,
       :oauth2_endpoint_token_grant_type_client_credentials_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_client_credentials_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_authorization_code_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_authorization_code_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_auth_methods_supported_callback,
       &Asteroid.OAuth2.Endpoint.token_endpoint_auth_methods_supported/0

# introspect

config :asteroid,
       :oauth2_endpoint_introspect_client_authorized,
       &Asteroid.OAuth2.Client.endpoint_introspect_authorized?/1

config :asteroid, :oauth2_endpoint_introspect_claims_resp, [
  "scope",
  "client_id",
  "username",
  "token_type",
  "exp",
  "iat",
  "nbf",
  "sub",
  "aud",
  "iss",
  "jti"
]

config :asteroid,
       :oauth2_endpoint_introspect_claims_resp_callback,
       &Asteroid.OAuth2.Introspect.endpoint_introspect_claims_resp/1

config :asteroid,
       :oauth2_endpoint_introspect_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_introspect_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# revoke

config :asteroid,
       :oauth2_endpoint_revoke_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# dynamic client registration

config :asteroid,
       :oauth2_endpoint_register_authorization_callback,
       &Asteroid.OAuth2.Register.request_authorized?/2

config :asteroid, :oauth2_endpoint_register_authorization_policy, :authorized_clients

config :asteroid, :oauth2_endpoint_register_additional_metadata_field, []

config :asteroid,
       :oauth2_endpoint_register_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_client_before_save_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_gen_client_id_callback,
       &Asteroid.OAuth2.Register.generate_client_id/2

config :asteroid,
       :oauth2_endpoint_register_gen_client_resource_id_callback,
       &Asteroid.OAuth2.Register.generate_client_resource_id/2

config :asteroid,
       :oauth2_endpoint_register_client_type_callback,
       &Asteroid.OAuth2.Register.client_type/1

# metadata

config :asteroid, :oauth2_endpoint_metadata_before_send_resp_callback, &Asteroid.Utils.id/1

config :asteroid, :oauth2_endpoint_metadata_before_send_conn_callback, &Asteroid.Utils.id/1

# discovery

config :asteroid, :oauth2_endpoint_discovery_keys_before_send_resp_callback, &Asteroid.Utils.id/1

config :asteroid, :oauth2_endpoint_discovery_keys_before_send_conn_callback, &Asteroid.Utils.id/1

####################### OAuth2 flows #################################

# ROPC

# 1 week
config :asteroid, :oauth2_flow_ropc_refresh_token_lifetime, 60 * 60 * 24 * 7

# client credentials

config :asteroid, :oauth2_flow_client_credentials_issue_refresh_token_init, false

# authorization code

# 1 week
config :asteroid, :oauth2_flow_authorization_code_refresh_token_lifetime, 3600 * 24 * 7

# implicit

config :asteroid, :oauth2_flow_implicit_access_token_lifetime, 60 * 60

# device authorization

config :asteroid,
       :oauth2_endpoint_device_authorization_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_device_authorization_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_device_code_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oauth2_flow_device_authorization_device_code_lifetime, 60 * 15

config :asteroid,
       :oauth2_flow_device_authorization_user_code_callback,
       &Asteroid.OAuth2.DeviceAuthorization.user_code/1

config :asteroid, :oauth2_flow_device_authorization_refresh_token_lifetime, 10 * 365 * 24 * 3600

config :asteroid,
       :oauth2_endpoint_token_grant_type_device_code_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_device_code_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_flow_device_authorization_rate_limiter,
       {Asteroid.OAuth2.DeviceAuthorization.RateLimiter.Hammer, []}

config :asteroid, :oauth2_flow_device_authorization_rate_limiter_interval, 5

# pkce

config :asteroid, :oauth2_pkce_policy, :optional

config :asteroid, :oauth2_pkce_allowed_methods, [:S256]

config :asteroid, :oauth2_pkce_must_use_callback, &Asteroid.OAuth2.Client.must_use_pkce?/1

####################### Scope configuration ##########################

config :asteroid, :scope_config, scopes: %{"openid" => []}

config :asteroid, :oauth2_scope_config, []

config :asteroid, :oauth2_flow_authorization_code_scope_config, []

config :asteroid, :oauth2_flow_implicit_scope_config, []

config :asteroid, :oauth2_flow_client_credentials_scope_config, []

config :asteroid, :oauth2_flow_ropc_scope_config, []

config :asteroid, :oauth2_flow_device_authorization_scope_config, []

####################### OAuth2 JAR #################################

config :asteroid, :oauth2_jar_enabled, :enabled

config :asteroid, :oauth2_jar_request_object_signing_alg_values_supported, ["RS256", "ES384"]

config :asteroid, :oauth2_jar_request_object_lifetime, 60

config :asteroid, :oauth2_jar_request_uri_get_opts,
  follow_redirect: false,
  # 20 ko
  max_body_length: 1024 * 20,
  timeout: 1000

####################### OIDC general configuration ################

config :asteroid, :oidc_id_token_lifetime, 60

config :asteroid, :oidc_id_token_lifetime_callback, &Asteroid.Token.IDToken.lifetime/1

config :asteroid, :oidc_id_token_signing_alg_values_supported, ["RS256"]

config :asteroid, :oidc_id_token_encryption_alg_values_supported, ["RSA1_5"]

config :asteroid, :oidc_id_token_encryption_enc_values_supported, ["A128GCM", "A192GCM"]

config :asteroid, :token_id_token_before_serialize_callback, &Asteroid.Utils.id_first_param/2

config :asteroid, :oidc_issue_id_token_on_refresh_callback, false

config :asteroid,
       :oidc_issue_id_token_on_refresh_callback,
       &Asteroid.Token.IDToken.issue_id_token?/1

config :asteroid, :oidc_acr_config,
  "3-factor": [
    callback: &AsteroidWeb.LOA3_webflow.start_webflow/2,
    auth_event_set: [["password", "otp", "webauthn"]]
  ],
  "2-factor": [
    callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
    auth_event_set: [["password", "<PASSWORD>"], ["password", "<PASSWORD>"], ["webauthn", "otp"]]
  ],
  "1-factor": [
    callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
    auth_event_set: [["password"], ["webauthn"]],
    default: true
  ]

config :asteroid, :oidc_subject_identifier_callback, &Asteroid.OIDC.subject_identifier/2

config :asteroid,
       :oidc_subject_identifier_pairwise_salt,
       Base.encode64(:crypto.strong_rand_bytes(24))

# userinfo

config :asteroid,
       :oidc_endpoint_userinfo_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oidc_endpoint_userinfo_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oidc_endpoint_userinfo_signature_alg_values_supported, ["RS256"]

config :asteroid, :oidc_endpoint_userinfo_encryption_alg_values_supported, ["RSA1_5"]

config :asteroid, :oidc_endpoint_userinfo_encryption_enc_values_supported, ["A128GCM", "A192GCM"]

config :asteroid, :oidc_claims_supported, ["email", "phone_number", "gender"]


<|start_filename|>lib/asteroid/oidc/authentication_event.ex<|end_filename|>
defmodule Asteroid.OIDC.AuthenticationEvent do
  @moduledoc """
  Convenience functions to work with authentication events

  The `%Asteroid.OIDC.AuthenticationEvent{}` object has the following meaningful members in
  its `:data` field:
  - `"name"`: the event name (`t:Asteroid.AuthenticationEvent.name/0`)
  - `"amr"`: the AMR of the event (`t:Asteroid.OIDC.amr/0`)
  - `"time"`: the time the authentication event occured (`non_neg_integer()`)
  - `"exp"`: expiration time (`non_neg_integer()`)
  """

  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.OIDC.AuthenticatedSession
  alias Asteroid.Token

  @type id :: String.t()

  @type name :: String.t()

  @enforce_keys [:id, :authenticated_session_id]

  defstruct [:id, :authenticated_session_id, :data]

  @type t :: %__MODULE__{
          id: id(),
          authenticated_session_id: AuthenticatedSession.id(),
          data: map()
        }

  @doc """
  Generates a new authentication event
  """

  @spec gen_new(AuthenticatedSession.id()) :: t()

  def gen_new(authenticated_session_id) do
    %__MODULE__{
      id: secure_random_b64(),
      authenticated_session_id: authenticated_session_id,
      data: %{}
    }
  end

  @doc """
  Gets an authentication event from the store
  """

  @spec get(id(), Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}

  def get(authentication_event_id, _opts \\ []) do
    ae_store_module = astrenv(:object_store_authentication_event)[:module]
    ae_store_opts = astrenv(:object_store_authentication_event)[:opts] || []

    case ae_store_module.get(authentication_event_id, ae_store_opts) do
      {:ok, authentication_event} when not is_nil(authentication_event) ->
        {:ok, authentication_event}

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "authentication event",
           reason: "not found in the store",
           id: authentication_event_id
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Returns all authentication events associated to an authenticated session
  """

  @spec get_from_authenticated_session_id(AuthenticatedSession.id()) :: [%__MODULE__{}]

  def get_from_authenticated_session_id(auth_session_id) do
    ae_store_module = astrenv(:object_store_authentication_event)[:module]
    ae_store_opts = astrenv(:object_store_authentication_event)[:opts] || []

    case ae_store_module.get_from_authenticated_session_id(auth_session_id, ae_store_opts) do
      {:ok, auth_event_ids} ->
        Enum.reduce(
          auth_event_ids,
          [],
          fn
            auth_event_id, acc ->
              case get(auth_event_id) do
                {:ok, auth_event} ->
                  [auth_event | acc]

                {:error, _} ->
                  acc
              end
          end
        )

      _ ->
        []
    end
  end

  @doc """
  Stores an authentication event
  """

  @spec store(t(), Context.t()) :: {:ok, t()} | {:error, any()}

  def store(authentication_event, ctx \\ %{})

  def store(authentication_event, ctx) do
    ae_store_module = astrenv(:object_store_authentication_event)[:module]
    ae_store_opts = astrenv(:object_store_authentication_event)[:opts] || []

    authentication_event =
      astrenv(:object_store_authentication_event_before_store_callback).(
        authentication_event,
        ctx
      )

    case ae_store_module.put(authentication_event, ae_store_opts) do
      :ok ->
        AuthenticatedSession.update_acr(authentication_event.authenticated_session_id)

        {:ok, authentication_event}

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Deletes an authentication event
  """

  @spec delete(t() | id()) :: :ok | {:error, any()}

  def delete(%__MODULE__{id: id, authenticated_session_id: authenticated_session_id}) do
    ae_store_as_module = astrenv(:object_store_authentication_event)[:module]
    ae_store_as_opts = astrenv(:object_store_authentication_event)[:opts] || []

    res = ae_store_as_module.delete(id, ae_store_as_opts)

    AuthenticatedSession.update_acr(authenticated_session_id)

    res
  end

  def delete(authentication_event_id) when is_binary(authentication_event_id) do
    {:ok, authentication_event} = get(authentication_event_id)

    delete(authentication_event)
  end

  @doc """
  Puts a value into the `data` field of authentication event

  If the value is `nil`, the authentication event is not changed and the filed is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(authentication_event, _key, nil), do: authentication_event

  def put_value(authentication_event, key, val) do
    %{authentication_event | data: Map.put(authentication_event.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of a authentication event

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(authentication_event, key) do
    %{authentication_event | data: Map.delete(authentication_event.data, key)}
  end
end


<|start_filename|>lib/asteroid/object_store/authenticated_session/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticatedSession.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.AuthenticatedSession` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.AuthenticatedSession.opts()`) are:
  - `:table_name`: an `atom()` for the table name. Defaults to `:asteroid_authenticated_session`
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased. One can use it to add additional indexes for clients or devices, e.g.:
  `tab_def: [index: :refresh_token, :subject_id, :client_id]`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `1800` (30 minutes)

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :subject_id, :data],
    index: [:subject_id]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  alias Asteroid.OIDC.AuthenticatedSession

  @behaviour Asteroid.ObjectStore.AuthenticatedSession

  @impl true

  def install(opts) do
    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    table_name = opts[:table_name] || :asteroid_authenticated_session

    tab_def =
      [
        attributes: [:id, :subject_id, :data],
        index: [:subject_id]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(table_name, tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created authenticated session store #{table_name}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: authenticated session store #{table_name} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create authenticated session store #{table_name} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 1800], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(authenticated_session_id, opts) do
    table_name = opts[:table_name] || :asteroid_authenticated_session

    case :mnesia.dirty_read(table_name, authenticated_session_id) do
      [] ->
        Logger.debug(
          "#{__MODULE__}: getting authenticated session `#{authenticated_session_id}`, " <>
            "value: `nil`"
        )

        {:ok, nil}

      [{^table_name, ^authenticated_session_id, subject_id, data}] ->
        authenticated_session = %AuthenticatedSession{
          id: authenticated_session_id,
          subject_id: subject_id,
          data: data
        }

        Logger.debug(
          "#{__MODULE__}: getting authenticated session `#{authenticated_session_id}`, " <>
            "value: `#{inspect(authenticated_session)}`"
        )

        {:ok, authenticated_session}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_subject_id(subject_id, opts) do
    table_name = opts[:table_name] || :asteroid_authenticated_session

    {:ok,
     for {_table_name, authenticated_session_id, _subject_id, _data} <-
           :mnesia.dirty_match_object({table_name, :_, subject_id, :_}) do
       authenticated_session_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(authenticated_session, opts) do
    table_name = opts[:table_name] || :asteroid_authenticated_session

    record = {
      table_name,
      authenticated_session.id,
      authenticated_session.subject_id,
      authenticated_session.data
    }

    :mnesia.dirty_write(table_name, record)

    Logger.debug(
      "#{__MODULE__}: stored authenticated session `#{authenticated_session.id}`, " <>
        "value: `#{inspect(authenticated_session)}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(authenticated_session_id, opts) do
    table_name = opts[:table_name] || :asteroid_authenticated_session

    :mnesia.dirty_delete(table_name, authenticated_session_id)

    Logger.debug("#{__MODULE__}: deleted authenticated session `#{authenticated_session_id}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oauth2/register_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.RegisterControllerTest do
  use AsteroidWeb.ConnCase, async: true

  alias OAuth2Utils.Scope
  alias AsteroidWeb.Router.Helpers, as: Routes
  alias Asteroid.Client
  alias Asteroid.Token.AccessToken

  # error cases

  test "invalid content-type", %{conn: conn} do
    assert_raise Plug.Parsers.UnsupportedMediaTypeError, fn ->
      conn
      |> put_req_header("content-type", "plain/text")
      |> post(Routes.register_path(conn, :handle), "Some plain text")
      |> json_response(400)
    end
  end

  test ":all policy, unauthenticated client authorized to create new clients", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number one",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    Process.put(:oauth2_endpoint_register_authorization_policy, :all)

    response =
      conn
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_one"
    assert response["token_endpoint_auth_method"] == "client_secret_basic"
    assert response["grant_types"] == ["authorization_code"]
    assert response["response_types"] == ["code"]
    assert {:ok, _} = Client.load_from_unique_attribute("client_id", response["client_id"])
  end

  test ":authenticated_clients policy, unauthenticated client not authorized to create new clients",
       %{conn: conn} do
    req_body = %{
      "client_name" => "<NAME>",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    Process.put(:oauth2_endpoint_register_authorization_policy, :authenticated_clients)

    conn = post(conn, Routes.register_path(conn, :handle), req_body)
    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")
  end

  test ":authenticated_clients policy, client with bad credentials not authorized to create new clients",
       %{conn: conn} do
    req_body = %{
      "client_name" => "Example client <NAME>",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    Process.put(:oauth2_endpoint_register_authorization_policy, :authenticated_clients)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "invalid"))
      |> post(Routes.register_path(conn, :handle), req_body)

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")
  end

  test ":authenticated_client policy, client authorized to create new clients", %{conn: conn} do
    req_body = %{
      "client_name" => "<NAME>",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    Process.put(:oauth2_endpoint_register_authorization_policy, :all)

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_three"
    assert response["token_endpoint_auth_method"] == "client_secret_basic"
    assert response["grant_types"] == ["authorization_code"]
    assert response["response_types"] == ["code"]
    assert {:ok, _} = Client.load_from_unique_attribute("client_id", response["client_id"])
  end

  test ":authorized_clients policy, unauthenticated client not authorized to create new clients",
       %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number four",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    response =
      conn
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(401)

    assert response["error"] == "invalid_client"
  end

  test ":authorized_clients policy, authenticated but unauthorized client not authorized to create new clients",
       %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number five",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unauthorized_client"
  end

  test ":authorized_clients policy, client authorized to create new clients, auth basic", %{
    conn: conn
  } do
    req_body = %{
      "client_name" => "Example client number six",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_six"
    assert response["token_endpoint_auth_method"] == "client_secret_basic"
    assert response["grant_types"] == ["authorization_code"]
    assert response["response_types"] == ["code"]
    assert {:ok, _} = Client.load_from_unique_attribute("client_id", response["client_id"])
  end

  test ":authorized_clients policy, client authorized to create new clients, auth bearer", %{
    conn: conn
  } do
    req_body = %{
      "client_name" => "Example client number six'",
      "redirect_uris" => ["https://www.example.com/redirect_uri"]
    }

    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", "asteroid.register")
      |> AccessToken.store()

    response =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(access_token))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_six'"
    assert response["token_endpoint_auth_method"] == "client_secret_basic"
    assert response["grant_types"] == ["authorization_code"]
    assert response["response_types"] == ["code"]
    assert {:ok, _} = Client.load_from_unique_attribute("client_id", response["client_id"])
  end

  test "invalid redirect uri", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number seven",
      "redirect_uris" => [
        "https://www.example.com/redirect_uri",
        "https://www.example2.com/redirect_uri",
        "invalid uri",
        "https://www.example3.com/redirect_uri"
      ]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_redirect_uri"
  end

  test "invalid token endpoint authentication method", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number eight",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "token_endpoint_auth_method" => "auth_invalid_scheme"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
  end

  test "invalid grant type authentication method", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number nine",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "grant_types" => ["authorization_code", "implicit", "invalid_grant_type"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "Invalid field `grant_types`"
  end

  test "invalid response type authentication method", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number ten",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "grant_types" => ["implicit", "authorization_code"],
      "response_types" => ["code", "invalid", "token"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "Invalid field `response_types`"
  end

  test "incoherent grant and response types 1", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number eleven",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "grant_types" => ["implicit", "client_credentials", "password"],
      "response_types" => ["code", "token"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "have missing mandatory grant type"
  end

  test "Scope not declared neither at the client level or in the conf", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number thirteen",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "scope" => "scp99"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "requested scopes"
  end

  test "jwks_uri and jwks at the same time is not allowed", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number fourteen",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "jwks_uri" => "https://appleid.apple.com/auth/keys",
      "jwks" => %{
        "keys" => [%{"e" => "AQAB", "n" => "<KEY>
   <KEY>
   <KEY>
   <KEY>nFPlxptw59A<KEY>KQt60s1SLboazxFKve
   qXC_jpLUt22OC6GUG63p-REw-ZOr3r845z50wMuzifQrMI9bQ", "kty" => "RSA"}]
      }
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "`jwks_uri` and `jwks` fields cannot be present"
  end

  test "software_id is not a string leads to rejection", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number fifteen",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "software_id" => 56
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "software_id"
  end

  test "software_version is not a string leads to rejection", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number sixteen",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "software_version" => ["v5.1.0"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "software_version"
  end

  test "authorization code requested without any redirect_uri", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number seventeen",
      "redirect_uris" => [],
      "grant_types" => ["authorization_code"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "Invalid field `redirect_uris`"
  end

  test "implicit requested without any redirect_uri", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number eighteen",
      "redirect_uris" => [],
      "grant_types" => ["implicit"],
      "response_types" => ["token"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
    assert response["error_description"] =~ "Invalid field `redirect_uris`"
  end

  test "redirect_uri is not a list", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number nineteen",
      "redirect_uris" => %{"key" => "value"},
      "grant_types" => ["implicit"],
      "response_types" => ["token"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_client_metadata"
  end

  test "mobile application redirect_uri", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number twenty",
      "redirect_uris" => ["com.example.app:/oauth2redirect/example-provider"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_twenty"
    assert response["token_endpoint_auth_method"] == "client_secret_basic"
    assert response["grant_types"] == ["authorization_code"]
    assert response["response_types"] == ["code"]
    assert {:ok, _} = Client.load_from_unique_attribute("client_id", response["client_id"])
  end

  test "additional metadata added to result pus all standard attributes", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number twenty one",
      "client_name#fr" => "Client d'exemple numéro un",
      "client_name#ru" => "Примерое приложение номер один",
      "redirect_uris" => [
        "https://www.example.com/auth",
        "https://www.example.org/auth",
        "com.example.app:/oauth2redirect/example-provider"
      ],
      "token_endpoint_auth_method" => "client_secret_basic",
      "grant_types" => ["authorization_code", "implicit", "client_credentials", "password"],
      "response_types" => ["code", "token"],
      "client_uri" => "https://www.example.com",
      "scope" => "scp1 scp2 scp3 scp4 scp5 scp6",
      "contacts" => ["<EMAIL>"],
      "tos_uri" => "https://www.example.com/tos",
      "policy_uri" => "https://www.example.com/policy",
      "jwks_uri" => "https://www.example.com/jwks",
      "software_id" => "client_01",
      "software_version" => "1.5.2",
      "field_1" => "value 1",
      "field_2" => "value 2",
      "field_3" => "value 3",
      "field_4" => "value 4"
    }

    Process.put(:scope_config, scopes: %{"scp1" => []})
    Process.put(:oauth2_scope_config, scopes: %{"scp2" => []})
    Process.put(:oauth2_flow_ropc_scope_config, scopes: %{"scp3" => []})
    Process.put(:oauth2_flow_client_credentials_scope_config, scopes: %{"scp4" => []})
    Process.put(:oauth2_flow_implicit_scope_config, scopes: %{"scp5" => []})
    Process.put(:oauth2_flow_authorization_code_scope_config, scopes: %{"scp6" => []})

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_twenty_one"
    assert response["client_name"] == "Example client number twenty one"
    assert response["client_name_i18n"]["fr"] == "Client d'exemple numéro un"
    assert response["client_name_i18n"]["ru"] == "Примерое приложение номер один"

    assert Enum.sort(response["redirect_uris"]) ==
             Enum.sort([
               "https://www.example.com/auth",
               "https://www.example.org/auth",
               "com.example.app:/oauth2redirect/example-provider"
             ])

    assert response["token_endpoint_auth_method"] == "client_secret_basic"

    assert Enum.sort(response["grant_types"]) ==
             Enum.sort(["authorization_code", "implicit", "client_credentials", "password"])

    assert Enum.sort(response["response_types"]) == ["code", "token"]
    assert response["client_uri"] == "https://www.example.com"

    assert Scope.Set.equal?(
             Scope.Set.new(Scope.Set.from_scope_param!(response["scope"])),
             Scope.Set.new(Scope.Set.from_scope_param!("scp1 scp2 scp3 scp4 scp5 scp6"))
           )

    assert response["contacts"] == ["<EMAIL>"]
    assert response["tos_uri"] == "https://www.example.com/tos"
    assert response["policy_uri"] == "https://www.example.com/policy"
    assert response["jwks_uri"] == "https://www.example.com/jwks"
    assert response["software_id"] == "client_01"
    assert response["software_version"] == "1.5.2"
    assert response["field_1"] == "value 1"
    assert response["field_2"] == "value 2"
    assert response["field_3"] == nil
    assert response["field_4"] == "value 4"

    assert {:ok, client} = Client.load_from_unique_attribute("client_id", response["client_id"])

    client =
      Client.fetch_attributes(client, [
        "client_name",
        "client_name_i18n",
        "redirect_uris",
        "token_endpoint_auth_method",
        "grant_types",
        "response_types",
        "client_uri",
        "scope",
        "contacts",
        "tos_uri",
        "policy_uri",
        "jwks_uri",
        "software_id",
        "software_version",
        "field_1",
        "field_2",
        "field_3",
        "field_4",
        "client_secret"
      ])

    assert client.attrs["client_id"] == "example_client_number_twenty_one"
    assert client.attrs["client_name"] == "Example client number twenty one"
    assert client.attrs["client_name_i18n"]["fr"] == "Client d'exemple numéro un"
    assert client.attrs["client_name_i18n"]["ru"] == "Примерое приложение номер один"

    assert Enum.sort(client.attrs["redirect_uris"]) ==
             Enum.sort([
               "https://www.example.com/auth",
               "https://www.example.org/auth",
               "com.example.app:/oauth2redirect/example-provider"
             ])

    assert client.attrs["token_endpoint_auth_method"] == "client_secret_basic"

    assert Enum.sort(client.attrs["grant_types"]) ==
             Enum.sort(["authorization_code", "implicit", "client_credentials", "password"])

    assert Enum.sort(client.attrs["response_types"]) == ["code", "token"]
    assert client.attrs["client_uri"] == "https://www.example.com"

    assert Scope.Set.equal?(
             Scope.Set.new(client.attrs["scope"]),
             Scope.Set.new(Scope.Set.from_scope_param!("scp1 scp2 scp3 scp4 scp5 scp6"))
           )

    assert client.attrs["contacts"] == ["<EMAIL>"]
    assert client.attrs["tos_uri"] == "https://www.example.com/tos"
    assert client.attrs["policy_uri"] == "https://www.example.com/policy"
    assert client.attrs["jwks_uri"] == "https://www.example.com/jwks"
    assert client.attrs["software_id"] == "client_01"
    assert client.attrs["software_version"] == "1.5.2"
    assert client.attrs["__asteroid_created_by_client_id"] == "client_confidential_1"
    assert client.attrs["field_1"] == "value 1"
    assert client.attrs["field_2"] == "value 2"
    assert client.attrs["field_3"] == nil
    assert client.attrs["field_4"] == "value 4"

    assert Expwd.secure_compare(response["client_secret"], client.attrs["client_secret"])
  end

  test "auto scope granted at the client config level", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number twenty two",
      "redirect_uris" => ["https://www.example.com/auth"],
      "scope" => "scp11 scp13"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_3", "password3"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_twenty_two"

    assert Scope.Set.equal?(
             Scope.Set.new(Scope.Set.from_scope_param!(response["scope"])),
             Scope.Set.new(Scope.Set.from_scope_param!("scp11 scp13 scp19 scp17 scp18"))
           )

    assert {:ok, client} = Client.load_from_unique_attribute("client_id", response["client_id"])

    client = Client.fetch_attributes(client, ["scope"])

    assert client.attrs["client_id"] == "example_client_number_twenty_two"
    assert client.attrs["client_name"] == "Example client number twenty two"

    assert Scope.Set.equal?(
             Scope.Set.new(client.attrs["scope"]),
             Scope.Set.new(Scope.Set.from_scope_param!("scp11 scp13 scp19 scp17 scp18"))
           )
  end

  test "valid jwks", %{conn: conn} do
    key_1 = %{"e" => "AQAB", "n" => "<KEY>", "kty" => "RSA"}

    key_2 = %{
      "kty" => "EC",
      "crv" => "P-256",
      "x" => "<KEY>",
      "y" => "<KEY>",
      "use" => "enc",
      "kid" => "1"
    }

    req_body = %{
      "client_name" => "Example client number twenty three",
      "redirect_uris" => ["https://www.example.com/redirect_uri"],
      "jwks" => %{"keys" => [key_1, key_2]}
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_twenty_three"
    assert response["token_endpoint_auth_method"] == "client_secret_basic"
    assert response["grant_types"] == ["authorization_code"]
    assert response["response_types"] == ["code"]

    assert {:ok, client} = Client.load_from_unique_attribute("client_id", response["client_id"])

    client = Client.fetch_attributes(client, ["jwks"])

    assert {:binary_data, :erlang.term_to_binary(key_1)} in client.attrs["jwks"]
    assert {:binary_data, :erlang.term_to_binary(key_2)} in client.attrs["jwks"]
  end

  test "client defaults grant type and auth method", %{conn: conn} do
    req_body = %{
      "client_name" => "Example client number twenty four",
      "redirect_uris" => ["https://www.example.com/auth"]
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_3", "<PASSWORD>"))
      |> post(Routes.register_path(conn, :handle), req_body)
      |> json_response(201)

    assert response["client_id"] == "example_client_number_twenty_four"
    assert response["token_endpoint_auth_method"] == "client_secret_post"

    assert Enum.sort(response["grant_types"]) ==
             Enum.sort(["authorization_code", "client_credentials", "password"])

    assert response["response_types"] == ["code"]

    assert {:ok, client} = Client.load_from_unique_attribute("client_id", response["client_id"])

    client =
      Client.fetch_attributes(client, [
        "client_id",
        "token_endpoint_auth_method",
        "grant_types",
        "response_types"
      ])

    assert client.attrs["client_id"] == "example_client_number_twenty_four"
    assert client.attrs["token_endpoint_auth_method"] == "client_secret_post"

    assert Enum.sort(client.attrs["grant_types"]) ==
             Enum.sort(["authorization_code", "client_credentials", "password"])

    assert client.attrs["response_types"] == ["code"]
  end

  defp basic_auth_header(client, secret) do
    "Basic " <> Base.encode64(client <> ":" <> secret)
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oauth2/token_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.TokenControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias AsteroidWeb.Router.Helpers, as: Routes
  alias Asteroid.Token.{RefreshToken, AccessToken, AuthorizationCode, DeviceCode}
  alias OAuth2Utils.Scope
  alias Asteroid.OAuth2
  alias Asteroid.Crypto

  ##########################################################################
  # General tests
  ##########################################################################

  test "no grant_type", %{conn: conn} do
    response =
      conn
      |> post(Routes.token_path(conn, :handle))
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "invalid content-type", %{conn: conn} do
    assert_raise Plug.Parsers.UnsupportedMediaTypeError, fn ->
      conn
      |> put_req_header("content-type", "plain/text")
      |> post(Routes.token_path(conn, :handle), "Some plain text")
      |> json_response(400)
    end
  end

  test "invalid grant_type", %{conn: conn} do
    response =
      conn
      |> post(
        Routes.token_path(conn, :handle),
        %{"grant_type" => "unknown"}
      )
      |> json_response(400)

    assert response["error"] == "unsupported_grant_type"
  end

  test "no credentials for confidential client", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    conn = post(conn, Routes.token_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  # next 2 tests testing that:
  #
  # If the
  # client attempted to authenticate via the "Authorization"
  # request header field, the authorization server MUST
  # respond with an HTTP 401 (Unauthorized) status code and
  # include the "WWW-Authenticate" response header field
  # matching the authentication scheme used by the client.
  #
  # from https://tools.ietf.org/html/rfc6749#section-5.2
  test "invalid basic credentials for confidential client", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("invalid_client", "secret"))
      |> post(Routes.token_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm=)

    refute Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer )

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "invalid bearer credentials for confidential client", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    conn =
      conn
      |> put_req_header("authorization", "Bearer we<PASSWORD>")
      |> post(Routes.token_path(conn, :handle), req_body)

    refute Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm=)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer )

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "public client with credentials does not authenticate", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>",
      "client_id" => "client_public_2"
    }

    conn = post(conn, Routes.token_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "public client with invalid client_id", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>",
      "client_id" => "nю-client"
    }

    response =
      conn
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "grant type not authorized for client", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "password2"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unauthorized_client"
  end

  test "Malformed scope", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>",
      "scope" => "scp1   scp5"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_scope"
  end

  ##########################################################################
  # ROPC tests
  ##########################################################################

  test "ropc missing parameter", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "password" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "ropc invalid username param", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "not\nweel-formed username",
      "password" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "ropc invalid password param", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "ropc invalid username & password", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_does_not_exist",
      "password" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "ropc valid username", %{conn: conn} do
    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, _} = AccessToken.get(response["access_token"])
    assert {:ok, _} = RefreshToken.get(response["refresh_token"])
    assert response["scope"] == nil
  end

  test "ropc valid username and scopes", %{conn: conn} do
    req_scope = MapSet.new(["scp3", "scp5", "scp6", "scp1"])

    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>",
      "scope" => Enum.join(req_scope, " ")
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])
    assert MapSet.equal?(MapSet.new(refresh_token.data["scope"]), req_scope)
    assert MapSet.equal?(MapSet.new(access_token.data["scope"]), req_scope)
    assert refresh_token.data["sub"] == req_body["username"]
    assert access_token.data["sub"] == req_body["username"]
    assert refresh_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["client_id"] == "client_confidential_1"
    assert response["scope"] == nil
  end

  test "ropc valid username and invalid scopes", %{conn: conn} do
    req_scope = MapSet.new(["scp3", "scp7", "scp6", "scp1"])

    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>",
      "scope" => Enum.join(req_scope, " ")
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_scope"
  end

  test "ropc additional scope added by callback", %{conn: conn} do
    req_scope = MapSet.new(["scp3", "scp5", "scp6", "scp1"])

    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>",
      "scope" => Enum.join(req_scope, " ")
    }

    f = fn
      scopes, _ctx ->
        Scope.Set.put(scopes, "scp99")
    end

    Process.put(:oauth2_scope_callback, f)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.put(req_scope, "scp99")
           )

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.put(req_scope, "scp99")
           )

    assert refresh_token.data["sub"] == req_body["username"]
    assert access_token.data["sub"] == req_body["username"]
    assert refresh_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["client_id"] == "client_confidential_1"

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.put(req_scope, "scp99")
           )
  end

  test "ropc access and refresh token lifetime limited by scope config", %{conn: conn} do
    req_scope = MapSet.new(["scp1", "scp2", "scp3", "scp4"])

    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>ftw",
      "scope" => Enum.join(req_scope, " ")
    }

    Process.put(:oauth2_flow_ropc_scope_config,
      scopes: %{
        "scp1" => [],
        "scp2" => [],
        "scp3" => [],
        "scp4" => [max_refresh_token_lifetime: 1000, max_access_token_lifetime: 30]
      }
    )

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)
    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert response["expires_in"] >= 29
    assert response["expires_in"] <= 31
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(req_scope)
           )

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(req_scope)
           )

    assert refresh_token.data["exp"] >= now() + 999
    assert refresh_token.data["exp"] <= now() + 1001
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31
  end

  test "ropc valid username, JWS access token", %{conn: conn} do
    Process.put(:oauth2_flow_ropc_access_token_serialization_format, :jws)
    Process.put(:oauth2_flow_ropc_access_token_signing_key, "key_auto_sig")
    Process.put(:oauth2_flow_ropc_access_token_signing_alg, "RS384")

    req_body = %{
      "grant_type" => "password",
      "username" => "user_1",
      "password" => "<PASSWORD>"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, _} = RefreshToken.get(response["refresh_token"])
    assert response["scope"] == nil

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, _, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], response["access_token"])
  end

  ##########################################################################
  # Client Credentials tests
  ##########################################################################

  test "client credentials public client with no credentials: authentication is mandatory", %{
    conn: conn
  } do
    req_body = %{
      "grant_type" => "client_credentials"
    }

    conn = post(conn, Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 401)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    assert response["error"] == "invalid_client"
  end

  test "client credentials confidential client with invalid credentials: authentication is mandatory",
       %{conn: conn} do
    req_body = %{
      "grant_type" => "client_credentials"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "invalid"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 401)

    # here we should only have one value in this header because the authentication failed, we
    # are not in the case there is NO authentication attempt
    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert response["error"] == "invalid_client"
  end

  test "client credentials valid confidential client authentication, token issued", %{conn: conn} do
    req_body = %{
      "grant_type" => "client_credentials"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert access_token.data["sub"] == nil
    assert access_token.data["client_id"] == "client_confidential_1"
    assert response["scope"] == nil
    assert response["refresh_token"] == nil
  end

  test "client credentials valid client authentication, token issued (incl. refresh token)", %{
    conn: conn
  } do
    req_body = %{
      "grant_type" => "client_credentials"
    }

    Process.put(:oauth2_flow_client_credentials_issue_refresh_token_init, true)
    Process.put(:oauth2_flow_client_credentials_refresh_token_lifetime, 60 * 60)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])
    assert refresh_token.data["sub"] == nil
    assert access_token.data["sub"] == nil
    assert refresh_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["client_id"] == "client_confidential_1"
    assert response["scope"] == nil
  end

  test "client credentials valid client authentication, access token issued with scopes", %{
    conn: conn
  } do
    req_scope = MapSet.new(["scp3", "scp5", "scp6", "scp1"])

    req_body = %{
      "grant_type" => "client_credentials",
      "scope" => Enum.join(req_scope, " ")
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert access_token.data["sub"] == nil
    assert access_token.data["client_id"] == "client_confidential_1"
    assert response["scope"] == nil
    assert response["refresh_token"] == nil
    assert Scope.Set.equal?(Scope.Set.new(access_token.data["scope"]), req_scope)
    assert response["scope"] == nil
  end

  test "client credentials additional scope added by callback", %{conn: conn} do
    req_scope = MapSet.new(["scp3", "scp5", "scp6", "scp1"])

    req_body = %{
      "grant_type" => "client_credentials",
      "scope" => Enum.join(req_scope, " ")
    }

    f = fn
      scopes, _ctx ->
        Scope.Set.put(scopes, "scp99")
    end

    Process.put(:oauth2_scope_callback, f)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert access_token.data["scope"] != nil
    assert access_token.data["sub"] == nil
    assert access_token.data["client_id"] == "client_confidential_1"

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.put(req_scope, "scp99")
           )
  end

  test "client credentials valid client authentication, invalid scope", %{conn: conn} do
    req_scope = MapSet.new(["scp3", "scp7", "scp6", "scp1"])

    req_body = %{
      "grant_type" => "client_credentials",
      "scope" => Enum.join(req_scope, " ")
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_scope"
  end

  test "client credentials access and refresh token lifetime limited by scope config", %{
    conn: conn
  } do
    req_scope = MapSet.new(["scp1", "scp2", "scp3", "scp4"])

    req_body = %{
      "grant_type" => "client_credentials",
      "scope" => Enum.join(req_scope, " ")
    }

    Process.put(:oauth2_flow_client_credentials_scope_config,
      scopes: %{
        "scp1" => [],
        "scp2" => [],
        "scp3" => [],
        "scp4" => [max_refresh_token_lifetime: 1000, max_access_token_lifetime: 30]
      }
    )

    Process.put(:oauth2_flow_client_credentials_issue_refresh_token_init, true)
    Process.put(:oauth2_flow_client_credentials_refresh_token_lifetime, 60_000)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert response["expires_in"] >= 29
    assert response["expires_in"] <= 31
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(req_scope)
           )

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(req_scope)
           )

    assert refresh_token.data["exp"] >= now() + 999
    assert refresh_token.data["exp"] <= now() + 1001
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31
  end

  test "client credentials valid confidential client authentication, JWS token issued", %{
    conn: conn
  } do
    Process.put(:oauth2_flow_client_credentials_access_token_serialization_format, :jws)
    Process.put(:oauth2_flow_client_credentials_access_token_signing_key, "key_auto_sig")
    Process.put(:oauth2_flow_client_credentials_access_token_signing_alg, "RS384")

    req_body = %{
      "grant_type" => "client_credentials"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert response["scope"] == nil
    assert response["refresh_token"] == nil

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, access_token_str, _} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["access_token"])

    access_token_data = Jason.decode!(access_token_str)

    assert access_token_data["sub"] == nil
    assert access_token_data["client_id"] == "client_confidential_1"
  end

  ##########################################################################
  # Refresh token grant types
  ##########################################################################

  test "refresh token public client with credentials: authentication is mandatory", %{conn: conn} do
    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => "<PASSWORD>"
    }

    conn = post(conn, Routes.token_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "refresh token missing refresh_token parameter", %{conn: conn} do
    req_body = %{
      "grant_type" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "refresh token invalid refresh token param", %{conn: conn} do
    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "refresh token valid request confidential client", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> RefreshToken.store(%{flow: :ropc})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"
  end

  # FIXME: start here
  test "refresh token valid request public client", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_public_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> RefreshToken.store(%{flow: :ropc})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id,
      "client_id" => "client_public_1"
    }

    conn =
      conn
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert access_token.data["client_id"] == "client_public_1"
  end

  test "refresh token valid request, issuance of new refresh token", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", ["scp3", "scp4", "scp1"])
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{:flow => :ropc})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    Process.put(:oauth2_flow_ropc_issue_refresh_token_refresh, true)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    refute refresh_token.id == RefreshToken.get(response["refresh_token"])
    assert {:ok, newly_issued_refresh_token} = RefreshToken.get(response["refresh_token"])
    assert {:error, _} = RefreshToken.get(refresh_token.id)
    assert newly_issued_refresh_token.data["client_id"] == refresh_token.data["client_id"]
    assert newly_issued_refresh_token.data["sub"] == refresh_token.data["sub"]
    assert newly_issued_refresh_token.data["iss"] == refresh_token.data["iss"]

    assert Scope.Set.equal?(
             Scope.Set.new(newly_issued_refresh_token.data["scope"]),
             Scope.Set.new(refresh_token.data["scope"])
           )
  end

  test "refresh token valid request, access and refresh token lifetime limited by scope config",
       %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", ["scp3", "scp4", "scp1"])
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{:flow => :ropc})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    Process.put(:oauth2_flow_ropc_scope_config,
      scopes: %{
        "scp1" => [],
        "scp2" => [],
        "scp3" => [],
        "scp4" => [max_refresh_token_lifetime: 1000, max_access_token_lifetime: 30]
      }
    )

    Process.put(:oauth2_flow_ropc_issue_refresh_token_refresh, true)

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert response["expires_in"] >= 29
    assert response["expires_in"] <= 31
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, newly_issued_refresh_token} = RefreshToken.get(response["refresh_token"])

    assert Scope.Set.equal?(
             Scope.Set.new(newly_issued_refresh_token.data["scope"]),
             Scope.Set.new(refresh_token.data["scope"])
           )

    assert newly_issued_refresh_token.data["exp"] >= now() + 999
    assert newly_issued_refresh_token.data["exp"] <= now() + 1001
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31
  end

  test "refresh token issued to different client_id", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_99")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "nonexistent refresh token", %{conn: conn} do
    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "invalid refresh token", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_99")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() - 42)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "refresh token valid request, check scopes of access token", %{conn: conn} do
    scopes = Enum.reduce(1..129, MapSet.new(), fn n, mapset -> MapSet.put(mapset, "scp#{n}") end)

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", scopes)
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert Scope.Set.equal?(Scope.Set.new(access_token.data["scope"]), scopes)
  end

  test "refresh token valid request subset of scope", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", ["scp3", "scp4", "scp1", "scp6"])
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id,
      "scope" => "scp1 scp4"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert access_token.data["client_id"] == "client_confidential_1"
    assert response["scope"] == nil

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(["scp1", "scp4"])
           )
  end

  test "refresh token invalid request, scope not included in refresh token", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", ["scp3", "scp4", "scp1", "scp6"])
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id,
      "scope" => "scp1 scp4 scp2"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_scope"
  end

  test "refresh token valid request confidential client, JWS access token", %{conn: conn} do
    Process.put(:oauth2_flow_ropc_access_token_serialization_format, :jws)
    Process.put(:oauth2_flow_ropc_access_token_signing_key, "key_auto_sig")
    Process.put(:oauth2_flow_ropc_access_token_signing_alg, "RS384")

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{flow: :ropc})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)

    response = json_response(conn, 200)

    assert "no-store" in Plug.Conn.get_resp_header(conn, "cache-control")
    assert "no-cache" in Plug.Conn.get_resp_header(conn, "pragma")
    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, access_token_str, _} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["access_token"])

    access_token_data = Jason.decode!(access_token_str)

    assert access_token_data["client_id"] == "client_confidential_1"
    assert access_token_data["sub"] == "user_1"
  end

  ##########################################################################
  # Authorization code grant types
  ##########################################################################

  test "grant type code not enabled", %{conn: conn} do
    req_body = %{
      "grant_type" => "authorization_code",
      "code" => "eigzjewochgwortuxwh",
      "redirect_uri" => "https://www.example.com"
    }

    Process.put(:oauth2_grant_types_enabled, [:password, :client_credentials, :refresh_token])

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unsupported_grant_type"
  end

  test "grant type code public client doesn't provide the client_id parameter", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_public_1")
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(401)

    assert response["error"] == "invalid_client"
  end

  test "grant type code missing code parameter", %{conn: conn} do
    req_body = %{
      "grant_type" => "authorization_code",
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "grant type code missing redirect_uri parameter", %{conn: conn} do
    req_body = %{
      "grant_type" => "authorization_code",
      "code" => "eigzjewochgwortuxwh"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "grant type code confidential client uses a code issued to another client", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_2")
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "grant type code confidential client uses a code issued to the request's client_id", %{
    conn: conn
  } do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_2")
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com",
      "client_id" => "client_confidential_2"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "grant type code public client uses a code issued to the erroneous request's client_id", %{
    conn: conn
  } do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_public_2")
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com",
      "client_id" => "client_public_1"
    }

    response =
      conn
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "grant type code confidential client uses an expired code", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("exp", now() - 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "grant type code confidential client uses an nonexistent code", %{conn: conn} do
    req_body = %{
      "grant_type" => "authorization_code",
      "code" => "eigzjewochgwortuxwh",
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "grant type code confidential client uses different redirect URIs", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://example.org/auth/web/"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "grant type code confidential client is not authorized to use this grant type", %{
    conn: conn
  } do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_2")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unauthorized_client"
  end

  test "grant type code success with refresh token without scopes", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    # lets sleep a bit so that we can check that iat and exp of released access and refresh
    # tokens are not the same as the ones of the authorization code, which would mean they
    # are copied while they shouldn't
    :timer.sleep(1500)

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert access_token.refresh_token_id == refresh_token.id

    assert access_token.data["client_id"] == code.data["client_id"]
    assert access_token.data["sub"] == code.data["sub"]
    assert access_token.data["issuer"] == code.data["issuer"]

    refute access_token.data["iat"] == code.data["iat"]
    refute access_token.data["exp"] == code.data["exp"]

    assert refresh_token.data["client_id"] == code.data["client_id"]
    assert refresh_token.data["sub"] == code.data["sub"]

    assert refresh_token.data["__asteroid_oauth2_initial_flow"] ==
             code.data["__asteroid_oauth2_initial_flow"]

    assert refresh_token.data["issuer"] == code.data["issuer"]

    refute refresh_token.data["iat"] == code.data["iat"]
    refute refresh_token.data["exp"] == code.data["exp"]
  end

  test "grant type code success with refresh token with scopes", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["scp1", "scp2", "scp5"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["scp1", "scp2", "scp5"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    # lets sleep a bit so that we can check that iat and exp of released access and refresh
    # tokens are not the same as the ones of the authorization code, which would mean they
    # are copied while they shouldn't
    :timer.sleep(1500)

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["scope"] == nil
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert access_token.refresh_token_id == refresh_token.id

    assert access_token.data["client_id"] == code.data["client_id"]
    assert access_token.data["sub"] == code.data["sub"]
    assert access_token.data["issuer"] == code.data["issuer"]

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )

    refute access_token.data["iat"] == code.data["iat"]
    refute access_token.data["exp"] == code.data["exp"]

    assert refresh_token.data["client_id"] == code.data["client_id"]
    assert refresh_token.data["sub"] == code.data["sub"]

    assert refresh_token.data["__asteroid_oauth2_initial_flow"] ==
             code.data["__asteroid_oauth2_initial_flow"]

    assert refresh_token.data["issuer"] == code.data["issuer"]

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )

    refute refresh_token.data["iat"] == code.data["iat"]
    refute refresh_token.data["exp"] == code.data["exp"]
  end

  test "grant type code success with refresh token with granted and requested scope different",
       %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["scp1", "scp2", "scp5"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["scp1", "scp5"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert Scope.Set.equal?(
             Scope.Set.new(response["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )

    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )
  end

  test "grant type code success with access and refresh token lifetime limited by scope config",
       %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value(
        "requested_scopes",
        Scope.Set.new(["scp1", "scp2", "scp3", "scp4"])
      )
      |> AuthorizationCode.put_value(
        "granted_scopes",
        Scope.Set.new(["scp1", "scp2", "scp3", "scp4"])
      )
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    # lets sleep a bit so that we can check that iat and exp of released access and refresh
    # tokens are not the same as the ones of the authorization code, which would mean they
    # are copied while they shouldn't
    :timer.sleep(1500)

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    Process.put(:oauth2_flow_authorization_code_scope_config,
      scopes: %{
        "scp1" => [],
        "scp2" => [],
        "scp3" => [],
        "scp4" => [max_refresh_token_lifetime: 1000, max_access_token_lifetime: 30]
      }
    )

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["expires_in"] >= 29
    assert response["expires_in"] <= 31
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])
    assert refresh_token.data["exp"] >= now() + 999
    assert refresh_token.data["exp"] <= now() + 1001
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(code.data["granted_scopes"])
           )
  end

  test "grant type code success without refresh token without scopes", %{conn: conn} do
    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    # lets sleep a bit so that we can check that iat and exp of released access and refresh
    # tokens are not the same as the ones of the authorization code, which would mean they
    # are copied while they shouldn't
    :timer.sleep(1500)

    Process.put(:oauth2_flow_authorization_code_issue_refresh_token_init, false)

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, access_token} = AccessToken.get(response["access_token"])
    assert response["refresh_token"] == nil

    assert access_token.refresh_token_id == nil

    assert access_token.data["client_id"] == code.data["client_id"]
    assert access_token.data["sub"] == code.data["sub"]
    assert access_token.data["issuer"] == code.data["issuer"]

    refute access_token.data["iat"] == code.data["iat"]
    refute access_token.data["exp"] == code.data["exp"]
  end

  test "grant type code success with refresh token without scopes, with JWS access token", %{
    conn: conn
  } do
    Process.put(:oauth2_flow_authorization_code_access_token_serialization_format, :jws)
    Process.put(:oauth2_flow_authorization_code_access_token_signing_key, "key_auto_sig")
    Process.put(:oauth2_flow_authorization_code_access_token_signing_alg, "RS512")

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    # lets sleep a bit so that we can check that iat and exp of released access and refresh
    # tokens are not the same as the ones of the authorization code, which would mean they
    # are copied while they shouldn't
    :timer.sleep(1500)

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, access_token_str, _} =
             JOSE.JWS.verify_strict(jwk, ["RS512"], response["access_token"])

    access_token_data = Jason.decode!(access_token_str)

    assert {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert access_token_data["client_id"] == code.data["client_id"]
    assert access_token_data["sub"] == code.data["sub"]
    assert access_token_data["issuer"] == code.data["issuer"]

    refute access_token_data["iat"] == code.data["iat"]
    refute access_token_data["exp"] == code.data["exp"]

    assert refresh_token.data["client_id"] == code.data["client_id"]
    assert refresh_token.data["sub"] == code.data["sub"]

    assert refresh_token.data["__asteroid_oauth2_initial_flow"] ==
             code.data["__asteroid_oauth2_initial_flow"]

    assert refresh_token.data["issuer"] == code.data["issuer"]

    refute refresh_token.data["iat"] == code.data["iat"]
    refute refresh_token.data["exp"] == code.data["exp"]
  end

  test "PKCE - grant type code success with plain code challenge method", %{conn: conn} do
    code_verifier = String.duplicate("r", 50)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge", code_verifier)
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge_method", "plain")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com",
      "code_verifier" => code_verifier
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, _} = AccessToken.get(response["access_token"])
    assert {:ok, _} = RefreshToken.get(response["refresh_token"])
  end

  test "PKCE - grant type code success with S256 code challenge method", %{conn: conn} do
    code_verifier = String.duplicate("r", 50)
    code_challenge = Base.url_encode64(:crypto.hash(:sha256, code_verifier), padding: false)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge", code_challenge)
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge_method", "S256")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com",
      "code_verifier" => code_verifier
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert is_integer(response["expires_in"])
    assert {:ok, _} = AccessToken.get(response["access_token"])
    assert {:ok, _} = RefreshToken.get(response["refresh_token"])
  end

  test "PKCE - grant type code failure with invalid code verifier", %{conn: conn} do
    code_verifier = String.duplicate("r", 50)
    code_challenge = Base.url_encode64(:crypto.hash(:sha256, code_verifier), padding: false)
    code_verifier = code_verifier <> "_invalid"

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge", code_challenge)
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge_method", "S256")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com",
      "code_verifier" => code_verifier
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "PKCE - grant type code failure with absent code verifier", %{conn: conn} do
    code_verifier = String.duplicate("r", 50)
    code_challenge = Base.url_encode64(:crypto.hash(:sha256, code_verifier), padding: false)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_confidential_1")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "code")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge", code_challenge)
      |> AuthorizationCode.put_value("__asteroid_oauth2_pkce_code_challenge_method", "S256")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  ##########################################################################
  # Device code grant type
  ##########################################################################

  test "Device flow: missing device_code parameter", %{conn: conn} do
    req_body = %{"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"}

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "Device flow: invalid device code", %{conn: conn} do
    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => "non_existant_code"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "Device flow: deactivated grant type", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI1")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "granted")
      |> DeviceCode.store(%{})

    Process.put(:oauth2_grant_types_enabled, [])

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unsupported_grant_type"
  end

  test "Device flow: client unauthorized to use device flow", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI2")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "granted")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unauthorized_client"
  end

  test "Device flow: device code issued to another client", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI3")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_2")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "granted")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_grant"
  end

  test "Device flow: authorization is pending", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI4")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "authorization_pending")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "authorization_pending"
  end

  test "Device flow: access was denied by the user", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI5")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "denied")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "access_denied"
  end

  test "Device flow: device code is expired", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI6")
      |> DeviceCode.put_value("exp", now() - 3)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "authorization_pending")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "expired_token"
  end

  test "Device flow: rate limited", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI7")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "authorization_pending")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    [_, _, _, _, _, response] =
      for _ <- 1..6 do
        conn
        |> put_req_header(
          "authorization",
          basic_auth_header("client_confidential_1", "<PASSWORD>")
        )
        |> post(Routes.token_path(conn, :handle), req_body)
        |> json_response(400)
      end

    assert response["error"] == "slow_down"
  end

  test "Device flow: success, no scopes", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI8")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", [])
      |> DeviceCode.put_value("granted_scopes", [])
      |> DeviceCode.put_value("status", "granted")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert is_binary(response["refresh_token"])
    assert is_binary(response["access_token"])
    assert is_integer(response["expires_in"])
    assert response["token_type"] == "bearer"
    assert response["scope"] == nil
    assert {:error, _} = DeviceCode.get(device_code.id)

    {:ok, access_token} = AccessToken.get(response["access_token"])
    {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert refresh_token.data["client_id"] == "client_confidential_1"
    assert refresh_token.data["sub"] == "user_1"
    assert refresh_token.data["scope"] == []
    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"
    assert access_token.data["scope"] == []
    assert access_token.refresh_token_id == refresh_token.id
  end

  test "Device flow: success with scopes (same as requested)", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI9")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", ["scp1", "scp4", "scp5"])
      |> DeviceCode.put_value("granted_scopes", ["scp1", "scp4", "scp5"])
      |> DeviceCode.put_value("status", "granted")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert is_binary(response["refresh_token"])
    assert is_binary(response["access_token"])
    assert is_integer(response["expires_in"])
    assert response["token_type"] == "bearer"
    assert response["scope"] == nil
    assert {:error, _} = DeviceCode.get(device_code.id)

    {:ok, access_token} = AccessToken.get(response["access_token"])
    {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert refresh_token.data["client_id"] == "client_confidential_1"
    assert refresh_token.data["sub"] == "user_1"

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(device_code.data["granted_scopes"])
           )

    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(device_code.data["granted_scopes"])
           )

    assert access_token.refresh_token_id == refresh_token.id
  end

  test "Device flow: success with scopes (different as requested)", %{conn: conn} do
    {:ok, device_code} =
      DeviceCode.gen_new(user_code: "ABCDEFGHI10")
      |> DeviceCode.put_value("exp", now() + 1000)
      |> DeviceCode.put_value("clid", "client_confidential_1")
      |> DeviceCode.put_value("sjid", "user_1")
      |> DeviceCode.put_value("requested_scopes", ["scp1", "scp4", "scp5"])
      |> DeviceCode.put_value("granted_scopes", ["scp1", "scp4"])
      |> DeviceCode.put_value("status", "granted")
      |> DeviceCode.store(%{})

    req_body = %{
      "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
      "device_code" => device_code.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert is_binary(response["refresh_token"])
    assert is_binary(response["access_token"])
    assert is_integer(response["expires_in"])
    assert response["token_type"] == "bearer"

    assert Enum.sort(Scope.Set.from_scope_param!(response["scope"])) ==
             Enum.sort(device_code.data["granted_scopes"])

    assert {:error, _} = DeviceCode.get(device_code.id)

    {:ok, access_token} = AccessToken.get(response["access_token"])
    {:ok, refresh_token} = RefreshToken.get(response["refresh_token"])

    assert refresh_token.data["client_id"] == "client_confidential_1"
    assert refresh_token.data["sub"] == "user_1"

    assert Scope.Set.equal?(
             Scope.Set.new(refresh_token.data["scope"]),
             Scope.Set.new(device_code.data["granted_scopes"])
           )

    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"

    assert Scope.Set.equal?(
             Scope.Set.new(access_token.data["scope"]),
             Scope.Set.new(device_code.data["granted_scopes"])
           )

    assert access_token.refresh_token_id == refresh_token.id
  end

  ##########################################################################
  # Helper functions
  ##########################################################################

  defp basic_auth_header(client, secret) do
    "Basic " <> Base.encode64(client <> ":" <> secret)
  end
end


<|start_filename|>lib/asteroid/web_flow.ex<|end_filename|>
defmodule Asteroid.WebFlow do
  @moduledoc """
  Convenience functions to work with web flows
  """

  import Asteroid.Utils

  alias Asteroid.OAuth2
  alias Asteroid.OIDC
  alias AsteroidWeb.AuthorizeController.Request

  @doc """
  Callback invoked to determine which callback function to call to continue the authorization
  process after the parameters were successfully verified

  If the protocol is OAuth2, it calls:
  - #{Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_web_authorization_callback)}
  if the flow is authorization code
  - #{Asteroid.Config.link_to_option(:oauth2_flow_implicit_web_authorization_callback)}
  if the flow is implicit

  If the protocol is OpenID Connect, it uses the
  #{Asteroid.Config.link_to_option(:oidc_acr_config)} configuration option to determine which
  callback to use:
  - if a preferred acr was computed, it uses its associated callback
  - otherwise, if one entry in the config is marked as `default: true`, it uses it

  If this configuration option is not used, it fall backs to:
  - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_web_authorization_callback)}
  if the flow is authorization code
  - #{Asteroid.Config.link_to_option(:oidc_flow_implicit_web_authorization_callback)}
  if the flow is implicit
  - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_web_authorization_callback)}
  if the flow is hybrid
  """

  @spec web_authorization_callback(Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t()) ::
          {:ok, (Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t() -> Plug.Conn.t())}
          | {:error, Exception.t()}

  def web_authorization_callback(_conn, authz_request) do
    oidc_acr_config = astrenv(:oidc_acr_config, [])

    case acceptable_acrs_for_scopes(authz_request) do
      nil ->
        default_web_authorization_callback(authz_request)

      acceptable_acrs ->
        case authz_request.claims do
          %{"id_token" => %{"acr" => %{"essential" => true, "values" => values}}} ->
            case Enum.find(values, &(&1 in acceptable_acrs)) do
              acr when is_binary(acr) ->
                {:ok, oidc_acr_config[acr][:callback]}

              nil ->
                {:error,
                 OAuth2.AccessDeniedError.exception(
                   reason: "Requested acrs could not be satisfied in regards to scopes config"
                 )}
            end

          %{"id_token" => %{"acr" => %{"essential" => true, "value" => value}}} ->
            if value in acceptable_acrs do
              {:ok, oidc_acr_config[value][:callback]}
            else
              {:error,
               OAuth2.AccessDeniedError.exception(
                 reason: "Requested acr could not be satisfied in regards to scopes config"
               )}
            end

          %{"id_token" => %{"acr" => %{"values" => values}}} ->
            case Enum.find(values, &(&1 in acceptable_acrs)) do
              acr when is_binary(acr) ->
                {:ok, oidc_acr_config[acr][:callback]}

              nil ->
                {:ok, first_acr_callback_from_config(acceptable_acrs)}
            end

          %{"id_token" => %{"acr" => %{"essential" => true, "value" => value}}} ->
            if value in acceptable_acrs do
              {:ok, oidc_acr_config[value][:callback]}
            else
              {:ok, first_acr_callback_from_config(acceptable_acrs)}
            end

          _ ->
            case authz_request.acr_values do
              acr_values when is_list(acr_values) ->
                case Enum.find(acr_values, &(&1 in acceptable_acrs)) do
                  acr when is_binary(acr) ->
                    {:ok, oidc_acr_config[acr][:callback]}

                  nil ->
                    {:ok, first_acr_callback_from_config(acceptable_acrs)}
                end

              nil ->
                default_web_authorization_callback(authz_request)
            end
        end
    end
  end

  @spec first_acr_callback_from_config(MapSet.t(OIDC.acr())) ::
          {:ok, (Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t() -> Plug.Conn.t())}
          | {:error, Exception.t()}

  defp first_acr_callback_from_config(acceptable_acrs) do
    Enum.find(
      astrenv(:oidc_acr_config, []),
      fn
        {acr, _acr_config} ->
          acr in acceptable_acrs
      end
    )
    |> case do
      {_acr, acr_config} ->
        {:ok, acr_config[:callback]}

      _ ->
        {:error,
         OAuth2.AccessDeniedError.exception(reason: "no suitable acr found in configuration")}
    end
  end

  @spec default_web_authorization_callback(AsteroidWeb.AuthorizeController.Request.t()) ::
          {:ok, (Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t() -> Plug.Conn.t())}
          | {:error, Exception.t()}

  defp default_web_authorization_callback(%Request{flow: :authorization_code}) do
    {:ok, astrenv(:oauth2_flow_authorization_code_web_authorization_callback)}
  end

  defp default_web_authorization_callback(%Request{flow: :implicit}) do
    {:ok, astrenv(:oauth2_flow_implicit_web_authorization_callback)}
  end

  defp default_web_authorization_callback(%Request{flow: flow} = authz_req)
       when flow in [
              :oidc_authorization_code,
              :oidc_implicit,
              :oidc_hybrid
            ] do
    oidc_acr_config = astrenv(:oidc_acr_config, [])

    maybe_preferred_acr =
      try do
        String.to_existing_atom(authz_req.preferred_acr)
      rescue
        _ ->
          nil
      end

    if oidc_acr_config[maybe_preferred_acr][:callback] do
      {:ok, oidc_acr_config[maybe_preferred_acr][:callback]}
    else
      maybe_default_callback =
        Enum.find_value(
          oidc_acr_config,
          fn
            {_acr, acr_config} ->
              if acr_config[:default] == true do
                acr_config[:callback]
              else
                nil
              end
          end
        )

      if maybe_default_callback do
        {:ok, maybe_default_callback}
      else
        case flow do
          :oidc_authorization_code ->
            {:ok, astrenv(:oidc_flow_authorization_code_web_authorization_callback)}

          :oidc_implicit ->
            {:ok, astrenv(:oidc_flow_implicit_web_authorization_callback)}

          :oidc_hybrid ->
            {:ok, astrenv(:oidc_flow_hybrid_web_authorization_callback)}
        end
      end
    end
  end

  @spec acceptable_acrs_for_scopes(Request.t()) :: MapSet.t(OIDC.acr()) | nil

  defp acceptable_acrs_for_scopes(authz_request) do
    scope_config = OAuth2.Scope.configuration_for_flow(authz_request.flow)

    # we use `nil` as the initial acc because it could be possible that one scope
    # require `acr1`, and another `acr2`. In such a case, we need to return an empty MapSet
    # (the condition cannot be satisfied). If there is no acr config at the scope level,
    # we have to return `nil` (and not an empty MapSet)

    Enum.reduce(
      authz_request.requested_scopes,
      nil,
      fn
        scope, acc ->
          case scope_config[:scopes][scope][:acceptable_acrs] do
            nil ->
              acc

            [_ | _] = acrs ->
              map_set = if acc, do: acc, else: MapSet.new()

              acrs
              |> Enum.reduce(MapSet.new(), &MapSet.put(&2, &1))
              |> MapSet.intersection(map_set)
          end
      end
    )
  end
end


<|start_filename|>lib/asteroid_web/views/device_view.ex<|end_filename|>
defmodule AsteroidWeb.DeviceView do
  @moduledoc false

  use AsteroidWeb, :view
end


<|start_filename|>lib/asteroid/oauth2/device_authorization/rate_limiter/hammer.ex<|end_filename|>
defmodule Asteroid.OAuth2.DeviceAuthorization.RateLimiter.Hammer do
  import Asteroid.Utils

  @behaviour Asteroid.OAuth2.DeviceAuthorization.RateLimiter

  @impl true

  def check(device_code_param, _opts) do
    interval = astrenv(:oauth2_flow_device_authorization_rate_limiter_interval)

    case Hammer.check_rate(device_code_param, interval * 1000, 1) do
      {:allow, _} ->
        :ok

      _ ->
        {:ok, {_count, _count_remaining, ms_to_next_bucket, _created_at, _updated_at}} =
          Hammer.inspect_bucket(device_code_param, interval * 1000, 1)

        {:rate_limited, div(ms_to_next_bucket, 1000) + 1}
    end
  end
end


<|start_filename|>lib/asteroid/token.ex<|end_filename|>
defmodule Asteroid.Token do
  require Logger

  @moduledoc """
  Types and exceptions for tokens
  """

  defmodule InvalidTokenError do
    @moduledoc """
    Error returned when a token was requested but an error happened when retrieving it
    """

    @enforce_keys [:sort, :id]

    defexception [:sort, :id, reason: ""]

    @type t :: %__MODULE__{
            sort: String.t(),
            reason: String.t(),
            id: String.t()
          }

    @impl true

    def message(%{sort: sort, reason: nil, id: id}) do
      "Invalid #{sort} `#{id}`"
    end

    def message(%{sort: sort, reason: reason, id: id}) do
      "Invalid #{sort} `#{id}`: #{reason}"
    end
  end

  @typedoc """
  Token sort (access token, refresh token, authorization code...)

  Token type is not used because it refers to how it's used by a client in OAuth2 specification
  (which defines the `"bearer"` token type).
  """

  @type sort :: :access_token | :refresh_token | :authorization_code

  @typedoc """
  String representation of a `t:sort/0`

  Must be the string conversion of a `t:sort/0` atom.
  """

  @type sort_str :: String.t()

  @typedoc """
  The different formats a token may have once serialized, i.e. send on the wire (in opposition
  to the token internally used by Asteroid)
  """

  @type serialization_format ::
          :opaque
          | :jws
          | :jwe
          | :saml1
          | :saml2

  @typedoc """
  String representation of a `t:serialization_format/0`

  Must be the string conversion of a `t:serialization_format/0` atom.
  """

  @type serialization_format_str :: String.t()

  @typedoc """
  Serialized token, as sent on the wire

  For instance, an refresh token as used internally by Asteroid would look like:

  ```elixir
  %Asteroid.Token.RefreshToken{
    data: %{},
    id: "F1XFSdm11N2XJ9OOPT7__Y0NqedjPwKgdT-ifQeuS3c",
    serialization_format: :opaque
  }
  ```

  One serialized, for instance sent by the `/token endpoint`, it looks like:
  ```elixir
  "F1XFSdm11N2XJ9OOPT7__Y0NqedjPwKgdT-ifQeuS3c"
  ```

  which is its id. If the serialization format had been `:jwt`, the serialized form would result
  in a JWT.
  """

  @type serialized_token :: String.t()
end


<|start_filename|>test/asteroid_web/controllers/device_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.DeviceControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias Asteroid.OAuth2
  alias Asteroid.Token.DeviceCode

  test "Authorization denied - access denied with no user code", %{conn: conn} do
    authz_request = %AsteroidWeb.DeviceController.Request{params: %{}}

    conn =
      conn
      |> bypass_through(AsteroidWeb.Router, [:browser])
      |> get("/device")
      |> AsteroidWeb.DeviceController.authorization_denied(%{
        authz_request: authz_request,
        user_code: nil,
        error: OAuth2.AccessDeniedError.exception(reason: "abc def")
      })

    assert html_response(conn, 200) =~ "Pairing denied"
  end

  test "Authorization denied - access denied with user code", %{conn: conn} do
    authz_request = %AsteroidWeb.DeviceController.Request{params: %{}}

    DeviceCode.gen_new(user_code: "ABCDEFGH1")
    |> DeviceCode.store(%{})

    conn =
      conn
      |> bypass_through(AsteroidWeb.Router, [:browser])
      |> get("/device")
      |> AsteroidWeb.DeviceController.authorization_denied(%{
        authz_request: authz_request,
        user_code: "ABCDEFGH1",
        error: OAuth2.AccessDeniedError.exception(reason: "abc def")
      })

    assert html_response(conn, 200) =~ "Pairing denied"

    assert {:ok, %DeviceCode{data: %{"status" => "denied"}}} =
             DeviceCode.get_from_user_code("ABCDEFGH1")
  end

  test "Authorization denied - server error", %{conn: conn} do
    authz_request = %AsteroidWeb.DeviceController.Request{params: %{}}

    conn =
      conn
      |> bypass_through(AsteroidWeb.Router, [:browser])
      |> get("/device")
      |> AsteroidWeb.DeviceController.authorization_denied(%{
        authz_request: authz_request,
        user_code: nil,
        error: OAuth2.ServerError.exception(reason: "abc def")
      })

    assert html_response(conn, 400) =~ "Pairing error"
  end

  test "Authorization granted", %{conn: conn} do
    authz_request = %AsteroidWeb.DeviceController.Request{params: %{}}

    DeviceCode.gen_new(user_code: "ABCDEFGH2")
    |> DeviceCode.put_value("exp", now() + 1000)
    |> DeviceCode.put_value("clid", "client_confidential_1")
    |> DeviceCode.store(%{})

    conn =
      conn
      |> bypass_through(AsteroidWeb.Router, [:browser])
      |> get("/device")
      |> AsteroidWeb.DeviceController.authorization_granted(%{
        authz_request: authz_request,
        user_code: "ABCDEFGH2",
        sjid: "user_1",
        granted_scopes: Scope.Set.new([])
      })

    assert html_response(conn, 200) =~ "Pairing successful"

    assert {:ok, %DeviceCode{data: %{"status" => "granted"}}} =
             DeviceCode.get_from_user_code("ABCDEFGH2")
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oauth2/token_controller_oidc_test.exs<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.TokenControllerOIDCTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias Asteroid.Token.{AuthorizationCode, RefreshToken}
  alias OAuth2Utils.Scope

  setup_all do
    rsa_enc_alg_all =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:enc)

    Client.gen_new(id: "client_oidc_test_sig")
    |> Client.add("client_id", "client_oidc_test_sig")
    |> Client.add("client_secret", "password1")
    |> Client.add("client_type", "confidential")
    |> Client.add("grant_types", ["authorization_code", "refresh_token"])
    |> Client.add("redirect_uris", ["https://www.example.com"])
    |> Client.add("id_token_signed_response_alg", "RS384")
    |> Client.add(
      "jwks",
      [rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)]
    )
    |> Client.store()

    Client.gen_new(id: "client_oidc_test_enc")
    |> Client.add("client_id", "client_oidc_test_enc")
    |> Client.add("client_secret", "password1")
    |> Client.add("client_type", "confidential")
    |> Client.add("grant_types", ["authorization_code"])
    |> Client.add("redirect_uris", ["https://www.example.com"])
    |> Client.add("id_token_signed_response_alg", "RS384")
    |> Client.add("id_token_encrypted_response_alg", "RSA1_5")
    |> Client.add("id_token_encrypted_response_enc", "A128GCM")
    |> Client.add(
      "jwks",
      [rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)]
    )
    |> Client.store()

    %{rsa_enc_alg_all: rsa_enc_alg_all}
  end

  test "grant type code flow code success with no refresh token", %{conn: conn} do
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_init, false)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_oidc_test_sig")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oidc_nonce", "xkgjuf9eswmgwszorixq")
      |> AuthorizationCode.put_value(
        "__asteroid_oidc_claims",
        ["email", "phone_number", "non_standard_claim_1"]
      )
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == "xkgjuf9eswmgwszorixq"
    assert id_token_data["sub"] == "user_1"

    assert id_token_data["at_hash"] ==
             TestOIDCHelpers.token_hash(digest, response["access_token"])

    assert id_token_data["nickname"] == nil
    assert id_token_data["email"] == "<EMAIL>"
    assert id_token_data["phone_number"] == "+3942390027"
    assert id_token_data["non_standard_claim_1"] == "some value"
  end

  test "grant type code flow code success with no refresh token & encrypted ID token",
       %{conn: conn, rsa_enc_alg_all: rsa_enc_alg_all} do
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_init, false)
    Process.put(:oidc_id_token_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oidc_id_token_encryption_enc_values_supported, ["A128GCM", "A192GCM"])

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_oidc_test_enc")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oidc_nonce", "xkgjuf9eswmgwszorixq")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_enc", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert is_integer(response["expires_in"])

    {id_token_jws, _jwe} = JOSE.JWE.block_decrypt(rsa_enc_alg_all, response["id_token"])

    {:ok, jwk_sig} = Crypto.Key.get("key_auto_sig")
    jwk_sig = JOSE.JWK.to_public(jwk_sig)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk_sig, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_enc"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == "xkgjuf9eswmgwszorixq"
    assert id_token_data["sub"] == "user_1"

    assert id_token_data["at_hash"] ==
             TestOIDCHelpers.token_hash(digest, response["access_token"])
  end

  test "grant type code flow code success with refresh token", %{conn: conn} do
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_init, true)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_oidc_test_sig")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oidc_nonce", "xkgjuf9eswmgwszorixq")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] != nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == "xkgjuf9eswmgwszorixq"
    assert id_token_data["sub"] == "user_1"

    assert id_token_data["at_hash"] ==
             TestOIDCHelpers.token_hash(digest, response["access_token"])
  end

  test "grant type code flow hybrid success with no refresh token", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_issue_refresh_token_init, false)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_oidc_test_sig")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "oidc_hybrid")
      |> AuthorizationCode.put_value("__asteroid_oidc_nonce", "xkgjuf9eswmgwszorixq")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.put_value(
        "__asteroid_oidc_claims",
        ["email", "phone_number", "non_standard_claim_1"]
      )
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == "xkgjuf9eswmgwszorixq"
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == nil
    assert id_token_data["nickname"] == nil
    assert id_token_data["email"] == "<EMAIL>"
    assert id_token_data["phone_number"] == "+3942390027"
    assert id_token_data["non_standard_claim_1"] == "some value"
  end

  test "grant type code flow hybrid success with refresh token", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_issue_refresh_token_init, true)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_oidc_test_sig")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "oidc_hybrid")
      |> AuthorizationCode.put_value("__asteroid_oidc_nonce", "xkgjuf9eswmgwszorixq")
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] != nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == "xkgjuf9eswmgwszorixq"
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == nil
  end

  test "grant type refresh token flow az code success with no new ID token", %{conn: conn} do
    Process.put(:oidc_flow_authorization_code_issue_id_token_refresh, false)
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_refresh, false)

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_oidc_test_sig")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", Scope.Set.new(["openid"]))
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert response["id_token"] == nil
    assert is_integer(response["expires_in"])
  end

  test "grant type refresh token flow az code success with new ID token", %{conn: conn} do
    Process.put(:oidc_flow_authorization_code_issue_id_token_refresh, true)
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_refresh, false)

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_oidc_test_sig")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", Scope.Set.new(["openid"]))
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> RefreshToken.put_value(
        "__asteroid_oidc_claims",
        ["email", "phone_number", "non_standard_claim_1"]
      )
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert response["id_token"] != nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == nil
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == nil
    assert id_token_data["nickname"] == nil
    assert id_token_data["email"] == "<EMAIL>"
    assert id_token_data["phone_number"] == "+3942390027"
    assert id_token_data["non_standard_claim_1"] == "some value"
  end

  test "grant type refresh token flow hybrid success with no new ID token", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_issue_id_token_refresh, false)
    Process.put(:oidc_flow_hybrid_issue_refresh_token_refresh, false)

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_oidc_test_sig")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", Scope.Set.new(["openid"]))
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "oidc_hybrid")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert response["id_token"] == nil
    assert is_integer(response["expires_in"])
  end

  test "grant type refresh token flow hybrid success with new ID token", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_issue_id_token_refresh, true)
    Process.put(:oidc_flow_hybrid_issue_refresh_token_refresh, false)

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_oidc_test_sig")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", Scope.Set.new(["openid"]))
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "oidc_hybrid")
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "password1"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert response["id_token"] != nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == nil
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == nil
  end

  test "grant type code flow code success with no refresh token, acr/amr/auth_time set",
       %{conn: conn} do
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_init, false)

    {:ok, code} =
      AuthorizationCode.gen_new()
      |> AuthorizationCode.put_value("client_id", "client_oidc_test_sig")
      |> AuthorizationCode.put_value("sub", "user_1")
      |> AuthorizationCode.put_value("requested_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("granted_scopes", Scope.Set.new(["openid"]))
      |> AuthorizationCode.put_value("iat", now())
      |> AuthorizationCode.put_value("exp", now() + 5)
      |> AuthorizationCode.put_value("redirect_uri", "https://www.example.com")
      |> AuthorizationCode.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> AuthorizationCode.put_value("__asteroid_oidc_nonce", "xkgjuf9eswmgwszorixq")
      |> AuthorizationCode.put_value("__asteroid_oidc_initial_acr", "urn:example:loa:loa1")
      |> AuthorizationCode.put_value("__asteroid_oidc_initial_amr", ["pwd", "<PASSWORD>"])
      |> AuthorizationCode.put_value("__asteroid_oidc_initial_auth_time", 100_000)
      |> AuthorizationCode.put_value("issuer", OAuth2.issuer())
      |> AuthorizationCode.store()

    req_body = %{
      "grant_type" => "authorization_code",
      "code" => AuthorizationCode.serialize(code),
      "redirect_uri" => "https://www.example.com"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == "xkgjuf9eswmgwszorixq"
    assert id_token_data["sub"] == "user_1"

    assert id_token_data["at_hash"] ==
             TestOIDCHelpers.token_hash(digest, response["access_token"])

    assert id_token_data["acr"] == "urn:example:loa:loa1"
    assert Enum.sort(id_token_data["amr"]) == ["phr", "pwd"]
    assert id_token_data["auth_time"] == 100_000
  end

  test "grant type refresh token flow az code success with new ID token, acramr/auth_time set",
       %{conn: conn} do
    Process.put(:oidc_flow_authorization_code_issue_id_token_refresh, true)
    Process.put(:oidc_flow_authorization_code_issue_refresh_token_refresh, false)

    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_oidc_test_sig")
      |> RefreshToken.put_value("sub", "user_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("scope", Scope.Set.new(["openid"]))
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "oidc_authorization_code")
      |> RefreshToken.put_value("__asteroid_oidc_initial_acr", "urn:example:loa:loa1")
      |> RefreshToken.put_value("__asteroid_oidc_initial_amr", ["pwd", "<PASSWORD>"])
      |> RefreshToken.put_value("__asteroid_oidc_initial_auth_time", 100_000)
      |> RefreshToken.store(%{})

    req_body = %{
      "grant_type" => "refresh_token",
      "refresh_token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_oidc_test_sig", "<PASSWORD>"))
      |> post(Routes.token_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["token_type"] == "bearer"
    assert response["access_token"] != nil
    assert response["refresh_token"] == nil
    assert response["id_token"] != nil
    assert is_integer(response["expires_in"])

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], response["id_token"])

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_test_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == nil
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == nil
    assert id_token_data["acr"] == "urn:example:loa:loa1"
    assert Enum.sort(id_token_data["amr"]) == ["phr", "pwd"]
    assert id_token_data["auth_time"] == 100_000
  end

  ##########################################################################
  # Helper functions
  ##########################################################################

  defp basic_auth_header(client, secret) do
    "Basic " <> Base.encode64(client <> ":" <> secret)
  end
end


<|start_filename|>test/asteroid_web/controllers/authorize_controller_response_mode.exs<|end_filename|>
defmodule AsteroidWeb.AuthorizeControllerTest do
  use AsteroidWeb.ConnCase, async: true

  alias Asteroid.Subject

  # initial request on /authorize

  test "Error case - invalid response mode OIDC", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "response_mode" => "invalid response mode",
      "scope" => "openid"
    }

    Process.put(:oauth2_response_mode_policy, :oidc_only)

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "invalid_request"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "invalid parameter"
  end

  test "Error case - invalid response mode OAuth2", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "response_mode" => "invalid response mode"
    }

    Process.put(:oauth2_response_mode_policy, :enabled)

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "invalid_request"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "invalid parameter"
  end

  test "Success case - no param default response mode OAuth2 flow code", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code"
    }

    Process.put(:oauth2_response_mode_policy, :enabled)

    Process.put(
      :oauth2_flow_authorization_code_web_authorization_callback,
      &Asteroid.Test.Callbacks.authorize_print_successful_request/2
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["response_mode"] == "query"
  end

  test "Success case - no param default response mode OAuth2 flow implicit", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "token"
    }

    Process.put(:oauth2_response_mode_policy, :enabled)

    Process.put(
      :oauth2_flow_implicit_web_authorization_callback,
      &Asteroid.Test.Callbacks.authorize_print_successful_request/2
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["response_mode"] == "fragment"
  end

  test "Success case - no param default response mode OIDC flow code", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid"
    }

    Process.put(:oauth2_response_mode_policy, :enabled)

    Process.put(
      :oidc_flow_authorization_code_web_authorization_callback,
      &Asteroid.Test.Callbacks.authorize_print_successful_request/2
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["response_mode"] == "query"
  end

  test "Success case - no param default response mode OIDC flow implicit", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "id_token token",
      "nonce" => "sxgjwzedrgdfchexgim",
      "scope" => "openid"
    }

    Process.put(:oauth2_response_mode_policy, :enabled)

    Process.put(
      :oidc_flow_implicit_web_authorization_callback,
      &Asteroid.Test.Callbacks.authorize_print_successful_request/2
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["response_mode"] == "fragment"
  end

  test "Success case - no param default response mode OIDC flow hybrid", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code id_token token",
      "scope" => "openid"
    }

    Process.put(:oauth2_response_mode_policy, :enabled)

    Process.put(
      :oidc_flow_hybrid_web_authorization_callback,
      &Asteroid.Test.Callbacks.authorize_print_successful_request/2
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["response_mode"] == "fragment"
  end

  # returning from an authorization process

  test "Success case - access granted query response mode OIDC hybrid flow", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_id_token_signing_key, "key_auto")
    Process.put(:oidc_flow_hybrid_access_token_lifetime, 43)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_hybrid,
      response_type: :"code id_token token",
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(["scp1", "scp2", "scp3"]),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: MapSet.new(["scp3"])
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["code"] != nil
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["access_token"] != nil
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["token_type"] == "Bearer"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["expires_in"] == "43"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["id_token"] != nil
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["scope"] == "scp3"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["state"] ==
             "sxgjwzedrgdfchexgim"

    assert URI.parse(redirected_to(conn)).fragment == nil
  end

  test "Success case - access granted fragment response mode OAuth2 azcode flow", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :fragment,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: MapSet.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert URI.decode_query(URI.parse(redirected_to(conn)).fragment)["code"] != nil

    assert URI.decode_query(URI.parse(redirected_to(conn)).fragment)["state"] ==
             "sxgjwzedrgdfchexgim"

    assert URI.parse(redirected_to(conn)).query == nil
  end

  test "Success case - access granted form_post response mode OIDC hybrid flow", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_id_token_signing_key, "key_auto")
    Process.put(:oidc_flow_hybrid_access_token_lifetime, 43)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_hybrid,
      response_type: :"code id_token token",
      response_mode: :form_post,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(["scp1", "scp2", "scp3"]),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      conn
      |> bypass_through(AsteroidWeb.Router, [:browser])
      # |> Plug.Conn.put_private(:plug_skip_csrf_protection, true)
      |> get("/authorize")
      |> AsteroidWeb.AuthorizeController.authorization_granted(%{
        authz_request: authz_request,
        subject: Subject.load("user_1") |> elem(1),
        granted_scopes: MapSet.new(["scp3"])
      })

    assert html_response(conn, 200) =~ ~s(name="code")
    assert html_response(conn, 200) =~ ~s(name="access_token")
    assert html_response(conn, 200) =~ ~s(name="token_type")
    assert html_response(conn, 200) =~ ~s(name="expires_in")
    assert html_response(conn, 200) =~ ~s(name="id_token")
    assert html_response(conn, 200) =~ ~s(name="scope")
    assert html_response(conn, 200) =~ ~s(name="state")
  end
end


<|start_filename|>lib/asteroid_web/controllers/device_controller.ex<|end_filename|>
defmodule AsteroidWeb.DeviceController do
  use AsteroidWeb, :controller

  require Logger

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias Asteroid.OAuth2
  alias Asteroid.Subject
  alias Asteroid.Token.DeviceCode

  defmodule Request do
    @moduledoc """
    Struct with the necessary information to process an web authorization request for the device
    authorization flow
    """

    defstruct [
      :params
    ]

    @type t :: %__MODULE__{
            params: map()
          }
  end

  @doc false

  @spec pre_authorize(Plug.Conn.t(), map()) :: Plug.Conn.t()

  def pre_authorize(conn, params) do
    authz_request = %Request{params: params}

    astrenv(:oauth2_flow_authorization_code_web_authorization_callback).(conn, authz_request)
  end

  @doc """
  Callback to be called when the authorization is granted, typically after user code verification,
  authentication and authorization (approving scopes) process

  The `opts` parameter is a `map()` whose keys are (all **mandatory**):
  - `:authz_request`: the initial `Request.t()` authorization request
  - `:user_code`: the user code (`t:Asteroid.OAuth2.DeviceAuthorization.user_code/0`) that has
  ben inputed by the user and optionnaly verified within the web flow
  - `:sjid`: the `t:Asteroid.Subject.id/0` of the user having approved the request
  - `:granted_scopes`: a `MapSet.t()` for the granted scope. If none was granted (because none
  were requested, or because user did not authorize them), a empty `t:Scope.Set.t/0` must be
  set
  """

  @spec authorization_granted(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()

  def authorization_granted(conn, opts) do
    case DeviceCode.get_from_user_code(opts[:user_code]) do
      {:ok, device_code} ->
        Logger.debug("#{__MODULE__}: authorization granted with params: `#{inspect(opts)}`")

        {:ok, subject} = Subject.load(opts[:sjid])

        ctx =
          %{}
          |> Map.put(:endpoint, :device)
          |> Map.put(:flow, :device_authorization)
          |> Map.put(:requested_scopes, Scope.Set.new(device_code.data["requested_scopes"] || []))
          |> Map.put(:granted_scopes, opts[:granted_scopes])
          |> Map.put(:subject, subject)
          |> Map.put(:flow_result, opts)
          |> Map.put(:conn, conn)

        granted_scopes = astrenv(:oauth2_scope_callback).(opts[:granted_scopes], ctx)

        device_code
        |> DeviceCode.put_value("sjid", opts[:sjid])
        |> DeviceCode.put_value("granted_scopes", Scope.Set.to_list(granted_scopes))
        |> DeviceCode.put_value("status", "granted")
        |> DeviceCode.store(ctx)

        conn
        |> put_flash(:info, "Pairing successful")
        |> put_status(200)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.DeviceView)
        |> render("device_authorization_granted.html")

      {:error, e} ->
        conn
        |> assign(:exception, e)
        |> assign(:authz_request, opts[:authz_request])
        |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
        |> put_status(400)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.DeviceView)
        |> render("device_authorization_error.html")
    end
  end

  @doc """
  Callback to be called when the authorization is denied, either by the user or by the
  server

  The options are a `map()` with the following keys (all **mandatory**):
  - `:authz_request`: the initial `t:AsteroidWeb.AuthorizeController.Request.t/0` authorization
  request
  - `:user_code`: the user code, if any was validated or entered by the user, to be marked
  as denied or `nil` if it was not verified
  - `:error`: one of the following exceptions:
    - `t:Asteroid.OAuth2.AccessDeniedError.t/0` when the request was denied either because of
    server policy or because of the user refusal
    - `t:Asteroid.OAuth2.ServerError.t/0` in case of server error
    - `t:Asteroid.OAuth2.TemporarilyUnavailableError.t/0` when the service is temporarily
    unavailable. Can be useful for maintenance mode
  """

  @spec authorization_denied(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()

  def authorization_denied(conn, %{error: %OAuth2.AccessDeniedError{}, user_code: nil} = opts) do
    Logger.debug(
      "#{__MODULE__}: authorization denied (#{inspect(opts[:authz_request])}) with " <>
        "reason: `#{Exception.message(opts[:error])}`"
    )

    conn
    |> assign(:exception, opts[:error])
    |> assign(:authz_request, opts[:authz_request])
    |> put_status(200)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.DeviceView)
    |> render("device_authorization_denied.html")
  end

  def authorization_denied(
        conn,
        %{error: %OAuth2.AccessDeniedError{}, user_code: user_code} = opts
      ) do
    Logger.debug(
      "#{__MODULE__}: authorization denied (#{inspect(opts[:authz_request])}) with " <>
        "reason: `#{Exception.message(opts[:error])}` and user code `#{user_code}`"
    )

    case DeviceCode.get_from_user_code(user_code) do
      {:ok, device_code} ->
        ctx =
          %{}
          |> Map.put(:endpoint, :device)
          |> Map.put(:flow, :device_authorization)
          |> Map.put(:requested_scopes, Scope.Set.new(device_code.data["scope"] || []))
          |> Map.put(:flow_result, opts[:error])
          |> Map.put(:conn, conn)

        device_code
        |> DeviceCode.put_value("status", "denied")
        |> DeviceCode.store(ctx)

        conn
        |> assign(:exception, opts[:error])
        |> assign(:authz_request, opts[:authz_request])
        |> put_status(200)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.DeviceView)
        |> render("device_authorization_denied.html")

      {:error, e} ->
        conn
        |> assign(:exception, e)
        |> assign(:authz_request, opts[:authz_request])
        |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
        |> put_status(400)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.DeviceView)
        |> render("device_authorization_error.html")
    end
  end

  def authorization_denied(conn, opts) do
    Logger.debug(
      "#{__MODULE__}: authorization denied (#{inspect(opts[:authz_request])}) with " <>
        "reason: `#{Exception.message(opts[:error])}`"
    )

    conn
    |> assign(:exception, opts[:error])
    |> assign(:authz_request, opts[:authz_request])
    |> put_flash(:error, "An error has occured (#{Exception.message(opts[:error])})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.DeviceView)
    |> render("device_authorization_error.html")
  end
end


<|start_filename|>lib/asteroid/object_store/authenticated_session.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticatedSession do
  @moduledoc """
  Behaviour for authenticated session store
  """

  @type opts :: Keyword.t()

  @doc """
  Installs the authenticated session store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the authenticated session store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the authenticated session store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an authenticated session from its id

  Returns `{:ok, %Asteroid.OIDC.AuthenticatedSession{}}` if the authenticated session exists
  and `{:ok, nil}` otherwise.
  """

  @callback get(Asteroid.OIDC.AuthenticatedSession.id(), opts()) ::
              {:ok, Asteroid.OIDC.AuthenticatedSession.t() | nil}
              | {:error, any()}

  @doc """
  Returns all the *authenticated session ids* of a subject
  """

  @callback get_from_subject_id(Asteroid.Subject.id(), opts()) ::
              {:ok, [Asteroid.OIDC.AuthenticatedSession.id()]} | {:error, any()}

  @doc """
  Stores an authenticated session

  If the authenticated session already exists, all of its data should be erased.
  """

  @callback put(Asteroid.OIDC.AuthenticatedSession.t(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an authenticated session
  """

  @callback delete(Asteroid.OIDC.AuthenticatedSession.id(), opts()) :: :ok | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1
end


<|start_filename|>lib/asteroid/crypto/cache/cache.ex<|end_filename|>
defmodule Asteroid.Crypto.Key.Cache do
  @moduledoc """
  JWK cache behaviour specification
  """

  defmodule NotFoundError do
    @moduledoc """
    Error returned when the key can not be found in the JWK cache
    """

    defexception []

    @impl true

    def message(_), do: "Key not found in JWK cache"
  end

  alias Asteroid.Crypto

  @type opts :: Keyword.t()

  @doc """
  Starts the JWK cache (non-supervised)

  Does the intiialization work if necessary.
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the JWK cache (supervised)

  Does the intiialization work if necessary.
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns a key from the cache
  """

  @callback get(Crypto.Key.name(), opts()) :: {:ok, %JOSE.JWK{}} | {:error, Exception.t()}

  @doc """
  Stores a key in the cache
  """

  @callback put(Crypto.Key.name(), %JOSE.JWK{}, opts()) :: :ok | {:error, Exception.t()}

  @doc """
  Deletes a key from the cache
  """

  @callback delete(Crypto.Key.name(), opts()) :: :ok | {:error, Exception.t()}

  @doc """
  Returns all keys
  """

  @callback get_all(opts()) :: {:ok, [{Crypto.Key.name(), %JOSE.JWK{}}]} | {:error, Exception.t()}

  @optional_callbacks start: 1,
                      start_link: 1
end


<|start_filename|>lib/asteroid/oauth2/jar.ex<|end_filename|>
defmodule Asteroid.OAuth2.JAR do
  @moduledoc """
  Functions to work with JWT Secured Authorization Request (JAR)
  """

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias AsteroidWeb.Router.Helpers, as: Routes

  defmodule RequestNotSupportedError do
    @moduledoc """
    Error returned when requesting with a JAR request object is not supported
    """

    @enforce_keys [:request_object]

    defexception [:request_object]

    @type t :: %__MODULE__{
            request_object: String.t()
          }

    @impl true

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "use of JAR request objects is disabled" <>
            " (current config: #{inspect(astrenv(:oauth2_jar_enabled))})"

        :normal ->
          "use of JAR request objects is disabled"

        :minimal ->
          ""
      end
    end
  end

  defmodule RequestURINotSupportedError do
    @moduledoc """
    Error returned when requesting with a JAR request object URI is not supported
    """

    defexception []

    @impl true

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "use of JAR request URIs is disabled" <>
            " (current config: #{inspect(astrenv(:oauth2_jar_enabled))})"

        :normal ->
          "use of JAR request object URIs is disabled"

        :minimal ->
          ""
      end
    end
  end

  defmodule InvalidRequestURIError do
    @moduledoc """
    Error returned when requesting with a JAR request object URI fails
    """

    defexception [:reason]

    @type t :: %__MODULE__{
            reason: String.t()
          }

    @impl true

    def message(%{reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          reason

        :normal ->
          reason

        :minimal ->
          ""
      end
    end
  end

  defmodule InvalidRequestObjectError do
    @moduledoc """
    Error returned when parsing and validatin a JAR object request fails
    """

    @enforce_keys [:request_object]

    defexception [:reason, :request_object]

    @type t :: %__MODULE__{
            reason: String.t(),
            request_object: String.t()
          }

    @impl true

    def message(%{reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          reason

        :normal ->
          reason

        :minimal ->
          ""
      end
    end
  end

  @doc """
  Parses and verifies a request object
  """

  @spec verify_and_parse(String.t()) :: {:ok, map()} | {:error, Exception.t()}

  def verify_and_parse(request_object_str) do
    if is_binary(decode_jwe_header(request_object_str)["enc"]) do
      case decrypt_jwe(request_object_str) do
        {:ok, jws} ->
          verify_and_parse_jws(jws)

        {:error, _} = error ->
          error
      end
    else
      verify_and_parse_jws(request_object_str)
    end
  rescue
    e ->
      {:error, e}
  end

  @spec decode_jwe_header(String.t()) :: map()

  defp decode_jwe_header(request_object_str) do
    request_object_str
    |> String.split(".")
    |> List.first()
    |> Base.url_decode64!(padding: false)
    |> Jason.decode!()
  rescue
    _ ->
      raise InvalidRequestObjectError,
        reason: "Invalid JWE header",
        request_object: request_object_str
  end

  @spec decrypt_jwe(String.t()) :: {:ok, String.t()} | {:error, Exception.t()}

  defp decrypt_jwe(jwe) do
    jwe_alg_supported = astrenv(:oauth2_jar_request_object_encryption_alg_values_supported) || []
    jwe_enc_supported = astrenv(:oauth2_jar_request_object_encryption_enc_values_supported) || []

    eligible_jwks =
      Crypto.Key.get_all()
      |> Enum.filter(fn
        %JOSE.JWK{fields: fields} ->
          (fields["use"] == "enc" or fields["use"] == nil) and
            (fields["key_ops"] in ["encrypt", "deriveKey"] or fields["key_ops"] == nil) and
            (fields["alg"] in jwe_alg_supported or fields["alg"] == nil)
      end)

    # as per https://mailarchive.ietf.org/arch/msg/oauth/pNMHnuBeBgF5zlea0RkA4bcmhz0

    jwe_header = decode_jwe_header(jwe)

    if jwe_header["alg"] in jwe_alg_supported and jwe_header["enc"] in jwe_enc_supported do
      maybe_payload =
        Enum.find_value(
          eligible_jwks,
          :decryption_failure,
          fn
            jwk ->
              try do
                case JOSE.JWE.block_decrypt(jwk, jwe) do
                  {message, %JOSE.JWE{}} when is_binary(message) ->
                    # FIXME: verify that result alg and enc in whitelist?
                    message

                  {:error, %JOSE.JWE{}} ->
                    false
                end
              rescue
                _ ->
                  false
              end
          end
        )

      case maybe_payload do
        payload when is_binary(payload) ->
          {:ok, payload}

        :decryption_failure ->
          {:error,
           InvalidRequestObjectError.exception(
             reason: "JWE decryption failure",
             request_object: jwe
           )}
      end
    else
      {:error,
       InvalidRequestObjectError.exception(
         reason: "Unsupported JWE `alg` or `enc`",
         request_object: jwe
       )}
    end
  end

  @spec verify_and_parse_jws(String.t()) :: {:ok, map()} | {:error, Exception.t()}

  defp verify_and_parse_jws(jws) do
    case Jason.decode(JOSE.JWS.peek_payload(jws)) do
      {:ok, unverified_params} ->
        case unverified_params do
          %{"client_id" => client_id} ->
            {:ok, client} =
              Client.load_from_unique_attribute(
                "client_id",
                client_id,
                attributes: ["jwks", "jwks_uri", "request_object_signing_alg"]
              )

            jws_alg_supported =
              if client.attrs["request_object_signing_alg"] do
                [client.attrs["request_object_signing_alg"]]
              else
                astrenv(:oauth2_jar_request_object_signing_alg_values_supported) || []
              end

            jws_alg = Jason.decode!(JOSE.JWS.peek_protected(jws))["alg"]

            if jws_alg == "none" and "none" in jws_alg_supported do
              case JOSE.JWS.verify_strict(%JOSE.JWK{}, ["none"], jws) do
                {true, payload, %JOSE.JWS{alg: {:jose_jws_alg_none, :none}}} ->
                  case Jason.decode(payload) do
                    {:ok, jwt} ->
                      {:ok, jwt}

                    {:error, _} ->
                      {:error,
                       InvalidRequestObjectError.exception(
                         reason: "JWT parsing error",
                         request_object: jws
                       )}
                  end

                _ ->
                  {:error,
                   InvalidRequestObjectError.exception(
                     reason: "JWS signature verification failed",
                     request_object: jws
                   )}
              end
            else
              case Client.get_jwks(client) do
                {:ok, keys} ->
                  eligible_jwks =
                    keys
                    |> Enum.map(&JOSE.JWK.from/1)
                    |> Enum.filter(fn
                      %JOSE.JWK{fields: fields} ->
                        (fields["use"] == "sig" or fields["use"] == nil) and
                          (fields["key_ops"] == "sign" or fields["key_ops"] == nil) and
                          (fields["alg"] in jws_alg_supported or fields["alg"] == nil)
                    end)

                  maybe_payload =
                    Enum.find_value(
                      eligible_jwks,
                      :signature_verification_failure,
                      fn
                        jwk ->
                          case JOSE.JWS.verify_strict(jwk, jws_alg_supported, jws) do
                            {true, message, %JOSE.JWS{}} ->
                              message

                            _ ->
                              false
                          end
                      end
                    )

                  case maybe_payload do
                    payload when is_binary(payload) ->
                      case Jason.decode(payload) do
                        {:ok, jwt} ->
                          if request_object_issuer_valid?(jwt, client) and
                               request_object_audience_valid?(jwt) do
                            {:ok, jwt}
                          else
                            {:error,
                             InvalidRequestObjectError.exception(
                               reason: "invalid `aud` or `iss` JWT field",
                               request_object: jws
                             )}
                          end

                        {:error, _} ->
                          {:error,
                           InvalidRequestObjectError.exception(
                             reason: "JWT parsing error",
                             request_object: jws
                           )}
                      end

                    :signature_verification_failure ->
                      {:error,
                       InvalidRequestObjectError.exception(
                         reason: "JWS signature verification failed",
                         request_object: jws
                       )}
                  end

                {:error, error} ->
                  {:error,
                   InvalidRequestObjectError.exception(
                     reason: "client keys could not be retrieved (#{inspect(error)})",
                     request_object: jws
                   )}
              end
            end

          _ ->
            {:error,
             InvalidRequestObjectError.exception(
               reason: "missing `client_id` in object request",
               request_object: jws
             )}
        end

      {:error, _} ->
        {:error,
         InvalidRequestObjectError.exception(
           reason: "invalid object request data",
           request_object: jws
         )}
    end
  rescue
    # raised by JOSE.JWS.peek/1
    _ ->
      {:error,
       InvalidRequestObjectError.exception(reason: "invalid JWS format", request_object: jws)}
  end

  @spec request_object_issuer_valid?(map(), Client.t()) :: boolean()

  defp request_object_issuer_valid?(request_object, client) do
    if astrenv(:oauth2_jar_request_object_verify_issuer, true) do
      client = Client.fetch_attributes(client, ["client_id"])

      request_object["iss"] == client.attrs["client_id"]
    else
      true
    end
  end

  @spec request_object_audience_valid?(map()) :: boolean()

  defp request_object_audience_valid?(request_object) do
    if astrenv(:oauth2_jar_request_object_verify_audience, true) do
      case request_object["aud"] do
        aud when is_list(aud) ->
          OAuth2.issuer() in aud

        aud when is_binary(aud) ->
          OAuth2.issuer() == aud
      end
    else
      true
    end
  end

  @doc """
  Retrieves a request object from a URI
  """

  @spec retrieve_object(String.t()) :: {:ok, String.t()} | {:error, Exception.t()}

  # as per the specification:
  #    The entire Request URI MUST NOT exceed 512 ASCII characters.  There
  #    are three reasons for this restriction.

  def retrieve_object(uri) when byte_size(uri) > 512 do
    {:error, InvalidRequestURIError.exception(reason: "`request_uri` too long")}
  end

  def retrieve_object(uri) do
    asteroid_request_object_store_prefix =
      Routes.request_object_url(AsteroidWeb.Endpoint, :create) <> "/"

    if String.starts_with?(uri, asteroid_request_object_store_prefix) do
      uri
      |> String.replace_prefix(asteroid_request_object_store_prefix, "")
      |> get_stored_request_object()
    else
      do_retrieve_object_external(uri)
    end
  end

  @spec do_retrieve_object_external(String.t()) :: {:ok, String.t()} | {:error, Exception.t()}

  defp do_retrieve_object_external(uri) do
    parsed_uri = URI.parse(uri)

    if parsed_uri.scheme == "https" do
      jar_request_uri_get_opts = astrenv(:oauth2_jar_request_uri_get_opts, [])

      case HTTPoison.get(uri, [], jar_request_uri_get_opts) do
        {:ok, %HTTPoison.Response{status_code: 200, headers: headers, body: body}} ->
          if headers_contain_content_type?(headers, "application", "jwt") do
            {:ok, body}
          else
            {:error,
             InvalidRequestURIError.exception(
               reason: "requesting the request uri resulted in incorrect `content-type`"
             )}
          end

        {:ok, %HTTPoison.Response{status_code: status_code}} ->
          {:error,
           InvalidRequestURIError.exception(
             reason: "requesting the request uri resulted in HTTP code #{status_code}"
           )}

        {:error, e} ->
          {:error, InvalidRequestURIError.exception(reason: Exception.message(e))}
      end
    else
      {:error, InvalidRequestURIError.exception(reason: "request URI must be HTTPS")}
    end
  end

  @doc """
  Retrieves an object from Asteroid's request object store
  """

  @spec get_stored_request_object(Asteroid.ObjectStore.GenericKV.key()) ::
          {:ok, String.t()}
          | {:error, Exception.t()}

  def get_stored_request_object(key) do
    module = astrenv(:object_store_request_object)[:module]
    opts = astrenv(:object_store_request_object)[:opts] || []

    req_obj_lifetime = astrenv(:oauth2_jar_request_object_lifetime, 0)

    now = now()

    case module.get(key, opts) do
      {:ok, %{"exp" => exp}} when now + req_obj_lifetime > exp ->
        {:error, InvalidRequestURIError.exception(reason: "object has expired")}

      {:ok, %{"request_object" => request_object}} ->
        {:ok, request_object}

      {:ok, nil} ->
        {:error, InvalidRequestURIError.exception(reason: "object could not be found")}

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Saves an object to Asteroid's request object store
  """

  @spec put_request_object(
          Asteroid.ObjectStore.GenericKV.key(),
          Asteroid.ObjectStore.GenericKV.value()
        ) ::
          :ok
          | {:error, any()}

  def put_request_object(key, value) do
    module = astrenv(:object_store_request_object)[:module]
    opts = astrenv(:object_store_request_object)[:opts] || []

    module.put(key, value, opts)
  end
end


<|start_filename|>lib/asteroid_web/controllers/api/oauth2/register_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.RegisterController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias Asteroid.Client
  alias Asteroid.OAuth2

  defmodule InvalidClientMetadataFieldError do
    @moduledoc """
    Error returned when client metadata is invalid
    """

    defexception [:field, :reason]

    @impl true

    def message(%{field: field, reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          case field do
            "token_endpoint_auth_method" ->
              "Invalid field `#{field}` (reason: #{reason}, supported methods:)" <>
                "#{inspect(astrenv(:oauth2_endpoint_token_auth_methods_supported_callback).())})"

            _ ->
              "Invalid field `#{field}` (reason: #{reason})"
          end

        :normal ->
          "Invalid field `#{field}`"

        :minimal ->
          ""
      end
    end
  end

  defmodule UnauthorizedRequestedScopesError do
    @moduledoc """
    Error returned when returning scopes are not allowed according to the policy (either the
    client's configuration or the scopes existing in the configuration options).
    """

    defexception [:scopes]

    @impl true

    def message(%{scopes: scopes}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "The following requested scopes are not allowed under the current policy: " <>
            Enum.join(scopes, " ")

        :normal ->
          "The requested scopes are not allowed under the current policy"

        :minimal ->
          ""
      end
    end
  end

  defmodule InvalidRedirectURIError do
    @moduledoc """
    Error raised when the one or more redirect URIs are invalid
    """

    defexception [:redirect_uri]

    @type t :: %__MODULE__{
            redirect_uri: String.t()
          }

    @impl true

    def message(%{redirect_uri: redirect_uri}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Invalid redirect URI `#{redirect_uri}`"

        :normal ->
          "Invalid redirect URI `#{redirect_uri}`"

        :minimal ->
          ""
      end
    end
  end

  def handle(conn, input_metadata) do
    maybe_authenticated_client =
      case OAuth2.Client.get_authenticated_client(conn) do
        {:ok, client} ->
          client

        {:error, _} ->
          nil
      end

    case OAuth2.Register.request_authorized?(conn, maybe_authenticated_client) do
      :ok ->
        :ok

      {:error, e} ->
        raise e
    end

    ctx =
      %{}
      |> Map.put(:endpoint, :register)
      |> Map.put(:conn, conn)
      |> put_if_not_nil(:client, maybe_authenticated_client)

    processed_metadata =
      %{}
      |> process_grant_types(maybe_authenticated_client, input_metadata)
      |> process_response_types(maybe_authenticated_client, input_metadata)
      |> check_grant_response_type_consistency()
      |> process_redirect_uris(input_metadata)
      |> process_token_endpoint_auth_method(maybe_authenticated_client, input_metadata)
      |> process_i18n_field(input_metadata, "client_name")
      |> process_i18n_field(input_metadata, "client_uri")
      |> process_i18n_field(input_metadata, "logo_uri")
      |> process_i18n_field(input_metadata, "tos_uri")
      |> process_i18n_field(input_metadata, "policy_uri")
      |> process_scope(maybe_authenticated_client, input_metadata)
      |> process_contacts(input_metadata)
      |> process_jwks_uri(input_metadata)
      |> process_jwks(input_metadata)
      |> process_software_id(input_metadata)
      |> process_software_version(input_metadata)
      |> process_oidc_application_type(input_metadata)
      |> process_oidc_sector_identifier_uri(input_metadata)
      |> process_oidc_subject_type(input_metadata)
      |> process_oidc_id_token_signed_response_alg(input_metadata)
      |> process_oidc_id_token_encrypted_response_alg(input_metadata)
      |> process_oidc_id_token_encrypted_response_enc(input_metadata)
      |> process_oidc_userinfo_signed_response_alg(input_metadata)
      |> process_oidc_userinfo_encrypted_response_alg(input_metadata)
      |> process_oidc_userinfo_encrypted_response_enc(input_metadata)
      |> process_oidc_request_object_signed_response_alg(input_metadata)
      |> process_oidc_request_object_encrypted_response_alg(input_metadata)
      |> process_oidc_request_object_encrypted_response_enc(input_metadata)
      |> process_oidc_default_max_age(input_metadata)
      |> process_oidc_require_auth_time(input_metadata)
      |> process_oidc_default_acr_values(input_metadata)
      |> process_additional_metadata_fields(maybe_authenticated_client, input_metadata)
      |> set_client_id(ctx)

    maybe_client_secret_and_hash =
      if processed_metadata["token_endpoint_auth_method"] == "client_secret_basic" or
           processed_metadata["token_endpoint_auth_method"] == "client_secret_post" do
        Expwd.Hashed.gen()
      end

    client_resource_id =
      astrenv(:oauth2_endpoint_register_gen_client_resource_id_callback).(processed_metadata, ctx)

    new_client =
      Enum.reduce(
        processed_metadata,
        Client.gen_new(id: client_resource_id),
        fn
          {k, v}, acc ->
            Client.add(acc, k, v)
        end
      )
      |> set_new_client_type()
      |> set_client_created_by(maybe_authenticated_client)

    new_client =
      if maybe_client_secret_and_hash do
        hashed_client_str =
          maybe_client_secret_and_hash
          |> elem(1)
          |> Expwd.Hashed.Portable.to_portable()

        Client.add(new_client, "client_secret", hashed_client_str)
      else
        new_client
      end

    :ok =
      new_client
      |> jwks_to_binary()
      |> astrenv(:oauth2_endpoint_register_client_before_save_callback).(ctx)
      |> Client.store()

    processed_metadata =
      if maybe_client_secret_and_hash do
        {client_secret, _client_secret_hashed} = maybe_client_secret_and_hash

        Map.put(processed_metadata, "client_secret", client_secret)
      else
        processed_metadata
      end
      |> astrenv(:oauth2_endpoint_register_before_send_resp_callback).(ctx)

    conn
    |> put_status(201)
    |> astrenv(:oauth2_endpoint_register_before_send_conn_callback).(ctx)
    |> json(processed_metadata)
  rescue
    e in [
      Asteroid.OAuth2.Client.AuthenticationError,
      Asteroid.OAuth2.Client.AuthorizationError,
      InvalidClientMetadataFieldError,
      InvalidRedirectURIError,
      UnauthorizedRequestedScopesError
    ] ->
      AsteroidWeb.Error.respond_api(conn, e)

    _ in Scope.Set.InvalidScopeParam ->
      AsteroidWeb.Error.respond_api(
        conn,
        InvalidClientMetadataFieldError.exception(
          field: "scope",
          reason: "malformed scope param"
        )
      )
  end

  @spec process_grant_types(map(), Client.t() | nil, map()) :: map()

  defp process_grant_types(
         processed_metadata,
         nil,
         %{"grant_types" => requested_grant_types}
       )
       when is_list(requested_grant_types) do
    enabled_grant_types =
      astrenv(:oauth2_grant_types_enabled)
      |> Enum.map(&to_string/1)
      |> MapSet.new()

    if MapSet.subset?(MapSet.new(requested_grant_types), enabled_grant_types) do
      Map.put(processed_metadata, "grant_types", requested_grant_types)
    else
      raise InvalidClientMetadataFieldError,
        field: "grant_types",
        reason:
          "one of the grant types could be granted as a result of applying the server policy"
    end
  end

  defp process_grant_types(
         processed_metadata,
         authenticated_client,
         %{"grant_types" => requested_grant_types}
       )
       when is_list(requested_grant_types) do
    conf_attr = "__asteroid_oauth2_endpoint_register_allowed_grant_types"

    client = Client.fetch_attributes(authenticated_client, [conf_attr])

    enabled_grant_types =
      astrenv(:oauth2_grant_types_enabled)
      |> Enum.map(&to_string/1)

    allowed_grant_types =
      MapSet.intersection(
        MapSet.new(client.attrs[conf_attr] || enabled_grant_types),
        MapSet.new(enabled_grant_types)
      )

    if MapSet.subset?(MapSet.new(requested_grant_types), allowed_grant_types) do
      Map.put(processed_metadata, "grant_types", requested_grant_types)
    else
      raise InvalidClientMetadataFieldError,
        field: "grant_types",
        reason:
          "one of the grant types could be granted as a result of applying the server policy"
    end
  end

  defp process_grant_types(_, _, %{"grant_types" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "grant_types",
      reason: "should be a list of strings"
  end

  defp process_grant_types(processed_metadata, nil, _) do
    Map.put(processed_metadata, "grant_types", ["authorization_code"])
  end

  defp process_grant_types(processed_metadata, authenticated_client, _) do
    attr = "__asteroid_oauth2_endpoint_register_default_grant_types"

    client = Client.fetch_attributes(authenticated_client, [attr])

    case client.attrs[attr] do
      l when is_list(l) ->
        Map.put(processed_metadata, "grant_types", l)

      nil ->
        Map.put(processed_metadata, "grant_types", ["authorization_code"])
    end
  end

  @spec process_response_types(map(), Client.t() | nil, map()) :: map()

  defp process_response_types(
         processed_metadata,
         nil,
         %{"response_types" => requested_response_types}
       )
       when is_list(requested_response_types) do
    enabled_response_types =
      astrenv(:oauth2_response_types_enabled)
      |> Enum.map(&to_string/1)
      |> MapSet.new()

    if MapSet.subset?(MapSet.new(requested_response_types), enabled_response_types) do
      Map.put(processed_metadata, "response_types", requested_response_types)
    else
      raise InvalidClientMetadataFieldError,
        field: "response_types",
        reason:
          "one of the response types could be granted as a result of applying the server policy"
    end
  end

  defp process_response_types(
         processed_metadata,
         authenticated_client,
         %{"response_types" => requested_response_types}
       )
       when is_list(requested_response_types) do
    conf_attr = "__asteroid_oauth2_endpoint_register_allowed_response_types"

    client = Client.fetch_attributes(authenticated_client, [conf_attr])

    enabled_response_types =
      astrenv(:oauth2_response_types_enabled)
      |> Enum.map(&to_string/1)

    allowed_response_types =
      MapSet.intersection(
        MapSet.new(client.attrs[conf_attr] || enabled_response_types),
        MapSet.new(enabled_response_types)
      )

    if MapSet.subset?(MapSet.new(requested_response_types), allowed_response_types) do
      Map.put(processed_metadata, "response_types", requested_response_types)
    else
      raise InvalidClientMetadataFieldError,
        field: "response_types",
        reason:
          "one of the response types could be granted as a result of applying the server policy"
    end
  end

  defp process_response_types(_, _, %{"response_types" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "response_types",
      reason: "should be a list of strings"
  end

  defp process_response_types(processed_metadata, nil, _) do
    Map.put(processed_metadata, "response_types", ["code"])
  end

  defp process_response_types(processed_metadata, authenticated_client, _) do
    attr = "__asteroid_oauth2_endpoint_register_default_response_types"

    client = Client.fetch_attributes(authenticated_client, [attr])

    case client.attrs[attr] do
      l when is_list(l) ->
        Map.put(processed_metadata, "response_types", l)

      nil ->
        Map.put(processed_metadata, "response_types", ["code"])
    end
  end

  @spec process_redirect_uris(map(), map()) :: map()

  defp process_redirect_uris(processed_metadata, input_metadata) do
    case input_metadata["redirect_uris"] do
      [_ | _] = redirect_uris ->
        Enum.each(
          redirect_uris,
          fn
            redirect_uri ->
              unless OAuth2.RedirectUri.valid?(redirect_uri) do
                raise InvalidRedirectURIError, redirect_uri: redirect_uri
              end
          end
        )

        Map.put(processed_metadata, "redirect_uris", redirect_uris)

      _ ->
        if Enum.any?(
             processed_metadata["grant_types"],
             fn
               grant_type ->
                 OAuth2Utils.uses_authorization_endpoint?(grant_type)
             end
           ) do
          raise InvalidClientMetadataFieldError,
            field: "redirect_uris",
            reason: "field is mandatory in regards to the requested grant types"
        else
          processed_metadata
        end
    end
  end

  @spec process_token_endpoint_auth_method(map(), Client.t() | nil, map()) :: map()

  defp process_token_endpoint_auth_method(
         processed_metadata,
         nil,
         %{"token_endpoint_auth_method" => token_endpoint_auth_method}
       )
       when is_binary(token_endpoint_auth_method) do
    auth_method_allowed_str =
      astrenv(:oauth2_endpoint_token_auth_methods_supported_callback).f()
      |> Enum.map(&to_string/1)

    if token_endpoint_auth_method in auth_method_allowed_str do
      Map.put(processed_metadata, "token_endpoint_auth_method", token_endpoint_auth_method)
    else
      raise InvalidClientMetadataFieldError,
        field: "token_endpoint_auth_method",
        reason: "The client authentication method `#{token_endpoint_auth_method}` is unsupported"
    end
  end

  defp process_token_endpoint_auth_method(
         processed_metadata,
         authenticated_client,
         %{"token_endpoint_auth_method" => token_endpoint_auth_method}
       )
       when is_binary(token_endpoint_auth_method) do
    auth_meth_client = "__asteroid_oauth2_endpoint_register_allowed_token_endpoint_auth_methods"

    client = Client.fetch_attributes(authenticated_client, [auth_meth_client])

    token_endpoint_auth_methods_supported =
      astrenv(:oauth2_endpoint_token_auth_methods_supported_callback).()

    auth_method_allowed_str =
      case client.attrs[auth_meth_client] do
        nil ->
          Enum.map(token_endpoint_auth_methods_supported, &to_string/1)

        l when is_list(l) ->
          MapSet.intersection(
            MapSet.new(l),
            token_endpoint_auth_methods_supported |> Enum.map(&to_string/1) |> MapSet.new()
          )
          |> MapSet.to_list()
      end

    if token_endpoint_auth_method in auth_method_allowed_str do
      Map.put(processed_metadata, "token_endpoint_auth_method", token_endpoint_auth_method)
    else
      raise InvalidClientMetadataFieldError,
        field: "token_endpoint_auth_method",
        reason: "The client authentication method `#{token_endpoint_auth_method}` is unsupported"
    end
  end

  defp process_token_endpoint_auth_method(
         _processed_metadata,
         _authenticated_client,
         %{"token_endpoint_auth_method" => _}
       ) do
    raise InvalidClientMetadataFieldError,
      field: "token_endpoint_auth_method",
      reason: "should be a list of strings"
  end

  defp process_token_endpoint_auth_method(processed_metadata, nil, _) do
    Map.put(processed_metadata, "token_endpoint_auth_method", "client_secret_basic")
  end

  defp process_token_endpoint_auth_method(processed_metadata, authenticated_client, _) do
    attr = "__asteroid_oauth2_endpoint_register_default_token_endpoint_auth_method"

    client = Client.fetch_attributes(authenticated_client, [attr])

    case client.attrs[attr] do
      auth_method when is_binary(auth_method) ->
        Map.put(processed_metadata, "token_endpoint_auth_method", auth_method)

      nil ->
        Map.put(processed_metadata, "token_endpoint_auth_method", "client_secret_basic")
    end
  end

  @spec process_scope(map(), Client.t() | nil, map()) :: map()

  defp process_scope(
         processed_metadata,
         nil,
         %{"scope" => scope_param}
       )
       when is_binary(scope_param) do
    requested_scopes = Scope.Set.from_scope_param!(scope_param)

    # let's compute all the available scopes for the flows associated to the accepted
    # grant types
    allowed_scopes_for_flows =
      Enum.reduce(
        processed_metadata["grant_types"],
        MapSet.new(),
        fn
          grant_type_str, acc ->
            grant_type = OAuth2.to_grant_type!(grant_type_str)

            case OAuth2.grant_type_to_flow(grant_type) do
              nil ->
                acc

              flow when is_atom(flow) ->
                Scope.Set.union(acc, OAuth2.Scope.scopes_for_flow(flow))
            end
        end
      )

    if Scope.Set.subset?(requested_scopes, allowed_scopes_for_flows) do
      Map.put(processed_metadata, "scope", Scope.Set.to_scope_param(requested_scopes))
    else
      raise UnauthorizedRequestedScopesError,
        scopes: Scope.Set.difference(requested_scopes, allowed_scopes_for_flows)
    end
  end

  defp process_scope(
         processed_metadata,
         authorized_client,
         %{"scope" => scope_param}
       )
       when is_binary(scope_param) do
    attr_allowed_scope = "__asteroid_oauth2_endpoint_register_allowed_scopes"
    attr_auto_scope = "__asteroid_oauth2_endpoint_register_auto_scopes"

    client = Client.fetch_attributes(authorized_client, [attr_allowed_scope, attr_auto_scope])

    client_auto_scopes = Scope.Set.new(client.attrs[attr_auto_scope] || [])

    requested_scopes = Scope.Set.from_scope_param!(scope_param)

    if client.attrs[attr_allowed_scope] do
      client_allowed_scopes = Scope.Set.new(client.attrs[attr_allowed_scope] || [])

      if Scope.Set.subset?(requested_scopes, client_allowed_scopes) do
        result_scopes = Scope.Set.union(requested_scopes, client_auto_scopes)

        Map.put(processed_metadata, "scope", Scope.Set.to_scope_param(result_scopes))
      else
        raise UnauthorizedRequestedScopesError,
          scopes: Scope.Set.difference(requested_scopes, client_allowed_scopes)
      end
    else
      # let's compute all the available scopes for the flows associated to the accepted
      # grant types
      allowed_scopes_for_flows =
        Enum.reduce(
          processed_metadata["grant_types"],
          MapSet.new(),
          fn
            grant_type_str, acc ->
              grant_type = OAuth2.to_grant_type!(grant_type_str)

              case OAuth2.grant_type_to_flow(grant_type) do
                nil ->
                  acc

                flow when is_atom(flow) ->
                  Scope.Set.union(acc, OAuth2.Scope.scopes_for_flow(flow))
              end
          end
        )

      if Scope.Set.subset?(requested_scopes, allowed_scopes_for_flows) do
        result_scopes = Scope.Set.union(requested_scopes, client_auto_scopes)

        Map.put(processed_metadata, "scope", Scope.Set.to_scope_param(result_scopes))
      else
        raise UnauthorizedRequestedScopesError,
          scopes: Scope.Set.difference(requested_scopes, allowed_scopes_for_flows)
      end
    end
  end

  defp process_scope(_processed_metadata, _client, %{"scope" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "scope",
      reason: "should be a string"
  end

  defp process_scope(processed_metadata, nil, _input_metadata) do
    # FIXME: allow default scopes here?

    processed_metadata
  end

  defp process_scope(processed_metadata, client, _input_metadata) do
    attr_auto_scope = "__asteroid_oauth2_endpoint_register_auto_scopes"

    client = Client.fetch_attributes(client, [attr_auto_scope])

    if client.attrs[attr_auto_scope] do
      Map.put(processed_metadata, "scope", client.attrs[attr_auto_scope])
    else
      processed_metadata
    end
  end

  @spec process_contacts(map(), map()) :: map()

  defp process_contacts(processed_metadata, %{"contacts" => contacts}) do
    case contacts do
      l when is_list(l) ->
        Enum.each(
          l,
          fn
            contact when is_binary(contact) ->
              :ok

            _ ->
              raise InvalidClientMetadataFieldError,
                field: "contacts",
                reason: "one of the list value is not a string"
          end
        )

        Map.put(processed_metadata, "contacts", contacts)

      _ ->
        raise InvalidClientMetadataFieldError,
          field: "contacts",
          reason: "not a list"
    end
  end

  defp process_contacts(processed_metadata, _input_metadata) do
    processed_metadata
  end

  @spec process_jwks_uri(map(), map()) :: map()

  defp process_jwks_uri(processed_metadata, %{"jwks_uri" => jwks_uri}) do
    case URI.parse(jwks_uri) do
      # we force it to be an HTTPS URL because the spec is unclear about it and to avoid
      # schemes such as file:///
      %URI{scheme: "https"} ->
        Map.put(processed_metadata, "jwks_uri", jwks_uri)

      _ ->
        raise InvalidClientMetadataFieldError,
          field: "jwks_uri",
          reason: "must be an https:// URL"
    end
  end

  defp process_jwks_uri(processed_metadata, _input_metadata) do
    processed_metadata
  end

  @spec process_jwks(map(), map()) :: map()

  defp process_jwks(%{"jwks_uri" => _}, %{"jwks" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "jwks",
      reason: "`jwks_uri` and `jwks` fields cannot be present at the same time"
  end

  defp process_jwks(processed_metadata, %{"jwks" => jwks}) do
    case jwks["keys"] do
      key_list when is_list(key_list) ->
        Enum.each(
          key_list,
          fn
            %{"kty" => _} ->
              :ok

            key ->
              raise InvalidClientMetadataFieldError,
                field: "jwks",
                reason: "invalid key `#{inspect(key)}`, must be a map and contain `kty`"
          end
        )

        Map.put(processed_metadata, "jwks", key_list)

      _ ->
        raise InvalidClientMetadataFieldError,
          field: "jwks",
          reason: "jwks must have a `keys` key containing a list of keys"
    end
  end

  defp process_jwks(processed_metadata, _input_metadata) do
    processed_metadata
  end

  @spec process_software_id(map(), map()) :: map()

  defp process_software_id(processed_metadata, %{"software_id" => software_id}) do
    if is_binary(software_id) do
      Map.put(processed_metadata, "software_id", software_id)
    else
      raise InvalidClientMetadataFieldError,
        field: "software_id",
        reason: "must be a string"
    end
  end

  defp process_software_id(processed_metadata, _) do
    processed_metadata
  end

  @spec process_software_version(map(), map()) :: map()

  defp process_software_version(processed_metadata, %{"software_version" => software_version}) do
    if is_binary(software_version) do
      Map.put(processed_metadata, "software_version", software_version)
    else
      raise InvalidClientMetadataFieldError,
        field: "software_version",
        reason: "must be a string"
    end
  end

  defp process_software_version(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_application_type(map(), map()) :: map()

  defp process_oidc_application_type(processed_metadata, %{"application_type" => app_type})
       when app_type in ["native", "web"] do
    Map.put(processed_metadata, "application_type", app_type)
  end

  defp process_oidc_application_type(_processed_metadata, %{"application_type" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "application_type",
      reason: "must be `web` or `native`"
  end

  defp process_oidc_application_type(processed_metadata, _) do
    Map.put(processed_metadata, "application_type", "web")
  end

  @doc """
  Verifies that the content of the document stored at a sector identifier URI is valid against
  a list of redirect URIs
  """

  @spec verify_sector_identifier_uri(String.t(), [OAuth2.RedirectUri.t(), ...]) ::
          :ok
          | {:error, %InvalidClientMetadataFieldError{}}

  def verify_sector_identifier_uri(sector_identifier_uri, redirect_uris) do
    case URI.parse(sector_identifier_uri) do
      %URI{scheme: "https"} ->
        case HTTPoison.get(sector_identifier_uri, [], []) do
          {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
            case Jason.decode(body) do
              {:ok, loaded_redirect_uris} when is_list(loaded_redirect_uris) ->
                if Enum.all?(redirect_uris, &(&1 in loaded_redirect_uris)) do
                  :ok
                else
                  {:error,
                   InvalidClientMetadataFieldError.exception(
                     field: "sector_identifier_uri",
                     reason:
                       "sector identifier URI redirect URIs not all included in" <>
                         "request redirect URIs"
                   )}
                end

              {:error, _e} ->
                {:error,
                 InvalidClientMetadataFieldError.exception(
                   field: "sector_identifier_uri",
                   reason: "invalid JSON content at sector identifier URI, must be a JSON list"
                 )}
            end

          {:ok, %HTTPoison.Response{status_code: st_code}} ->
            {:error,
             InvalidClientMetadataFieldError.exception(
               field: "sector_identifier_uri",
               reason: "requesting the sector identifier URI resulted in HTTP code #{st_code}"
             )}

          {:error, e} ->
            {:error,
             InvalidClientMetadataFieldError.exception(
               field: "sector_identifier_uri",
               reason: "HTTP error: " <> Exception.message(e)
             )}
        end

      _ ->
        {:error,
         InvalidClientMetadataFieldError.exception(
           field: "sector_identifier_uri",
           reason: "scheme must be `https`"
         )}
    end
  end

  @spec process_oidc_sector_identifier_uri(map(), map()) :: map()

  defp process_oidc_sector_identifier_uri(
         processed_metadata,
         %{"sector_identifier_uri" => sector_identifier_uri}
       ) do
    case verify_sector_identifier_uri(
           sector_identifier_uri,
           processed_metadata["redirect_uris"]
         ) do
      :ok ->
        Map.put(processed_metadata, "sector_identifier_uri", sector_identifier_uri)

      {:error, e} ->
        raise e
    end
  end

  defp process_oidc_sector_identifier_uri(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_subject_type(map(), map()) :: map()

  defp process_oidc_subject_type(processed_metadata, %{"subject_type" => "pairwise"}) do
    redir_hosts =
      Enum.reduce(
        processed_metadata,
        MapSet.new(),
        fn redirect_uri, acc -> MapSet.put(acc, URI.parse(redirect_uri).host) end
      )

    if MapSet.size(redir_hosts) > 1 and processed_metadata["sector_identifier_uri"] == nil do
      raise InvalidClientMetadataFieldError,
        field: "pairwise",
        reason:
          "`sector_identifier_uri` is mandatory when registering more than one " <>
            "host within redirect URIs"
    else
      Map.put(processed_metadata, "subject_type", "pairwise")
    end
  end

  defp process_oidc_subject_type(processed_metadata, %{"subject_type" => "public"}) do
    Map.put(processed_metadata, "subject_type", "public")
  end

  defp process_oidc_subject_type(_processed_metadata, %{"subject_type" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "subject_type",
      reason: "invalid subject type, must be `pairwise` or `public`"
  end

  defp process_oidc_subject_type(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_id_token_signed_response_alg(map(), map()) :: map()

  defp process_oidc_id_token_signed_response_alg(
         processed_metadata,
         %{"id_token_signed_response_alg" => id_token_signed_response_alg}
       ) do
    if id_token_signed_response_alg in astrenv(:oidc_id_token_supported_signing_algs, []) do
      Map.put(processed_metadata, "id_token_signed_response_alg", id_token_signed_response_alg)
    else
      raise InvalidClientMetadataFieldError,
        field: "id_token_signed_response_alg",
        reason: "value provided not in supported signing algs"
    end
  end

  defp process_oidc_id_token_signed_response_alg(processed_metadata, _) do
    Map.put(processed_metadata, "id_token_signed_response_alg", "RS256")
  end

  @spec process_oidc_id_token_encrypted_response_alg(map(), map()) :: map()

  defp process_oidc_id_token_encrypted_response_alg(
         processed_metadata,
         %{"id_token_encrypted_response_alg" => id_token_encrypted_response_alg}
       ) do
    if id_token_encrypted_response_alg in astrenv(:oidc_id_token_supported_encryption_algs, []) do
      Map.put(
        processed_metadata,
        "id_token_encrypted_response_alg",
        id_token_encrypted_response_alg
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "id_token_encrypted_response_alg",
        reason: "value provided not in supported encryption algs"
    end
  end

  defp process_oidc_id_token_encrypted_response_alg(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_id_token_encrypted_response_enc(map(), map()) :: map()

  defp process_oidc_id_token_encrypted_response_enc(
         processed_metadata,
         %{
           "id_token_encrypted_response_enc" => id_token_encrypted_response_enc,
           "id_token_encrypted_response_alg" => _
         }
       ) do
    if id_token_encrypted_response_enc in astrenv(:oidc_id_token_supported_encryption_encs, []) do
      Map.put(
        processed_metadata,
        "id_token_encrypted_response_enc",
        id_token_encrypted_response_enc
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "id_token_encrypted_response_enc",
        reason: "value provided not in supported encryption encs"
    end
  end

  defp process_oidc_id_token_encrypted_response_enc(
         _,
         %{"id_token_encrypted_response_enc" => _}
       ) do
    raise InvalidClientMetadataFieldError,
      field: "id_token_encrypted_response_enc",
      reason: "`id_token_encrypted_response_alg` must be registered along with this field"
  end

  defp process_oidc_id_token_encrypted_response_enc(
         processed_metadata,
         %{"id_token_encrypted_response_alg" => _}
       ) do
    Map.put(processed_metadata, "id_token_encrypted_response_enc", "A128CBC-HS256")
  end

  defp process_oidc_id_token_encrypted_response_enc(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_userinfo_signed_response_alg(map(), map()) :: map()

  defp process_oidc_userinfo_signed_response_alg(
         processed_metadata,
         %{"userinfo_signed_response_alg" => userinfo_signed_response_alg}
       ) do
    if userinfo_signed_response_alg in astrenv(:oidc_userinfo_supported_signing_algs, []) do
      Map.put(processed_metadata, "userinfo_signed_response_alg", userinfo_signed_response_alg)
    else
      raise InvalidClientMetadataFieldError,
        field: "userinfo_signed_response_alg",
        reason: "value provided not in supported signing algs"
    end
  end

  defp process_oidc_userinfo_signed_response_alg(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_userinfo_encrypted_response_alg(map(), map()) :: map()

  defp process_oidc_userinfo_encrypted_response_alg(
         processed_metadata,
         %{"userinfo_encrypted_response_alg" => userinfo_encrypted_response_alg}
       ) do
    if userinfo_encrypted_response_alg in astrenv(:oidc_userinfo_supported_encryption_algs, []) do
      Map.put(
        processed_metadata,
        "userinfo_encrypted_response_alg",
        userinfo_encrypted_response_alg
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "userinfo_encrypted_response_alg",
        reason: "value provided not in supported encryption algs"
    end
  end

  defp process_oidc_userinfo_encrypted_response_alg(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_userinfo_encrypted_response_enc(map(), map()) :: map()

  defp process_oidc_userinfo_encrypted_response_enc(
         processed_metadata,
         %{
           "userinfo_encrypted_response_enc" => userinfo_encrypted_response_enc,
           "userinfo_encrypted_response_alg" => _
         }
       ) do
    if userinfo_encrypted_response_enc in astrenv(:oidc_userinfo_supported_encryption_encs, []) do
      Map.put(
        processed_metadata,
        "userinfo_encrypted_response_enc",
        userinfo_encrypted_response_enc
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "userinfo_encrypted_response_enc",
        reason: "value provided not in supported encryption encs"
    end
  end

  defp process_oidc_userinfo_encrypted_response_enc(
         _processed_metadata,
         %{"userinfo_encrypted_response_enc" => _}
       ) do
    raise InvalidClientMetadataFieldError,
      field: "userinfo_encrypted_response_enc",
      reason: "`userinfo_encrypted_response_alg` must be registered along with this field"
  end

  defp process_oidc_userinfo_encrypted_response_enc(
         processed_metadata,
         %{"userinfo_encrypted_response_alg" => _}
       ) do
    Map.put(processed_metadata, "userinfo_encrypted_response_enc", "A128CBC-HS256")
  end

  defp process_oidc_userinfo_encrypted_response_enc(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_request_object_signed_response_alg(map(), map()) :: map()

  defp process_oidc_request_object_signed_response_alg(
         processed_metadata,
         %{"request_object_signed_response_alg" => request_object_signed_response_alg}
       ) do
    if request_object_signed_response_alg in astrenv(
         :oidc_request_object_supported_signing_algs,
         []
       ) do
      Map.put(
        processed_metadata,
        "request_object_signed_response_alg",
        request_object_signed_response_alg
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "request_object_signed_response_alg",
        reason: "value provided not in supported signing algs"
    end
  end

  defp process_oidc_request_object_signed_response_alg(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_request_object_encrypted_response_alg(map(), map()) :: map()

  defp process_oidc_request_object_encrypted_response_alg(
         processed_metadata,
         %{"request_object_encrypted_response_alg" => request_object_encrypted_response_alg}
       ) do
    if request_object_encrypted_response_alg in astrenv(
         :oidc_request_object_supported_encryption_algs,
         []
       ) do
      Map.put(
        processed_metadata,
        "request_object_encrypted_response_alg",
        request_object_encrypted_response_alg
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "request_object_encrypted_response_alg",
        reason: "value provided not in supported encryption algs"
    end
  end

  defp process_oidc_request_object_encrypted_response_alg(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_request_object_encrypted_response_enc(map(), map()) :: map()

  defp process_oidc_request_object_encrypted_response_enc(
         processed_metadata,
         %{
           "request_object_encrypted_response_enc" => request_object_encrypted_response_enc,
           "request_object_encrypted_response_alg" => _
         }
       ) do
    if request_object_encrypted_response_enc in astrenv(
         :oidc_request_object_supported_encryption_encs,
         []
       ) do
      Map.put(
        processed_metadata,
        "request_object_encrypted_response_enc",
        request_object_encrypted_response_enc
      )
    else
      raise InvalidClientMetadataFieldError,
        field: "request_object_encrypted_response_enc",
        reason: "value provided not in supported encryption encs"
    end
  end

  defp process_oidc_request_object_encrypted_response_enc(
         _processed_metadata,
         %{"request_object_encrypted_response_enc" => _request_object_encrypted_response_enc}
       ) do
    raise InvalidClientMetadataFieldError,
      field: "request_object_encrypted_response_enc",
      reason: "`request_object_encrypted_response_alg` must be registered along with this field"
  end

  defp process_oidc_request_object_encrypted_response_enc(
         processed_metadata,
         %{"request_object_encrypted_response_alg" => _}
       ) do
    Map.put(processed_metadata, "request_object_encrypted_response_enc", "A128CBC-HS256")
  end

  defp process_oidc_request_object_encrypted_response_enc(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_default_max_age(map(), map()) :: map()

  defp process_oidc_default_max_age(processed_metadata, %{"default_max_age" => default_max_age})
       when is_integer(default_max_age) do
    Map.put(processed_metadata, "default_max_age", default_max_age)
  end

  defp process_oidc_default_max_age(_processed_metadata, %{"default_max_age" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "default_max_age",
      reason: "must be an integer"
  end

  defp process_oidc_default_max_age(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_require_auth_time(map(), map()) :: map()

  defp process_oidc_require_auth_time(processed_metadata, %{
         "require_auth_time" => require_auth_time
       })
       when is_boolean(require_auth_time) do
    Map.put(processed_metadata, "require_auth_time", require_auth_time)
  end

  defp process_oidc_require_auth_time(_processed_metadata, %{"require_auth_time" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "require_auth_time",
      reason: "must be a boolean"
  end

  defp process_oidc_require_auth_time(processed_metadata, _) do
    processed_metadata
  end

  @spec process_oidc_default_acr_values(map(), map()) :: map()

  defp process_oidc_default_acr_values(processed_metadata, %{
         "default_acr_values" => default_acr_values
       })
       when is_list(default_acr_values) do
    acr_values_supported =
      Enum.map(astrenv(:oidc_acr_config, []), fn {k, _} -> Atom.to_string(k) end)

    if Enum.all?(default_acr_values, &(&1 in acr_values_supported)) do
      Map.put(processed_metadata, "default_acr_values", default_acr_values)
    else
      raise InvalidClientMetadataFieldError,
        field: "default_acr_values",
        reason: "one request acr value not in supported acr values"
    end
  end

  defp process_oidc_default_acr_values(_processed_metadata, %{"default_acr_values" => _}) do
    raise InvalidClientMetadataFieldError,
      field: "default_acr_values",
      reason: "must be a list of ACRs"
  end

  defp process_oidc_default_acr_values(processed_metadata, _) do
    processed_metadata
  end

  @spec process_additional_metadata_fields(map(), Client.t() | nil, map()) :: map()

  defp process_additional_metadata_fields(processed_metadata, nil, input_metadata) do
    Enum.reduce(
      astrenv(:oauth2_endpoint_register_additional_metadata_field, []),
      processed_metadata,
      fn
        additional_field, acc ->
          put_if_not_nil(acc, additional_field, input_metadata[additional_field])
      end
    )
  end

  defp process_additional_metadata_fields(processed_metadata, authorized_client, input_metadata) do
    add_met = "__asteroid_oauth2_endpoint_register_additional_metadata_fields"

    client = Client.fetch_attributes(authorized_client, [add_met])

    Enum.reduce(
      client.attrs[add_met] ||
        astrenv(:oauth2_endpoint_register_additional_metadata_field, []),
      processed_metadata,
      fn
        additional_field, acc ->
          put_if_not_nil(acc, additional_field, input_metadata[additional_field])
      end
    )
  end

  @spec set_client_id(map(), Asteroid.Context.t()) :: map()

  defp set_client_id(processed_metadata, ctx) do
    client_id =
      astrenv(:oauth2_endpoint_register_gen_client_id_callback).(processed_metadata, ctx)

    Map.put(processed_metadata, "client_id", client_id)
  end

  @spec set_new_client_type(Client.t()) :: Client.t()

  defp set_new_client_type(client) do
    case astrenv(:oauth2_endpoint_register_client_type_callback).(client) do
      :public ->
        Client.add(client, "client_type", "public")

      :confidential ->
        Client.add(client, "client_type", "private")
    end
  end

  @spec set_client_created_by(Client.t(), Client.t() | nil) :: Client.t()

  defp set_client_created_by(new_client, nil) do
    new_client
  end

  defp set_client_created_by(new_client, client) do
    client = Client.fetch_attributes(client, ["client_id"])

    Client.add(new_client, "__asteroid_created_by_client_id", client.attrs["client_id"])
  end

  @spec check_grant_response_type_consistency(map()) :: map()

  defp check_grant_response_type_consistency(processed_metadata) do
    case OAuth2.Register.grant_response_types_consistent?(
           processed_metadata["grant_types"],
           processed_metadata["response_types"]
         ) do
      :ok ->
        processed_metadata

      {:error, error_str} ->
        raise InvalidClientMetadataFieldError, field: "response_types", reason: error_str
    end
  end

  @spec jwks_to_binary(Client.t()) :: Client.t()

  defp jwks_to_binary(%Client{:attrs => %{"jwks" => jwks}} = client) do
    jwks_binary = for jwk <- jwks, do: {:binary_data, :erlang.term_to_binary(jwk)}

    client
    |> Client.remove("jwks")
    |> Client.add("jwks", jwks_binary)
  end

  defp jwks_to_binary(client) do
    client
  end

  @spec process_i18n_field(map(), map(), String.t()) :: map()

  defp process_i18n_field(processed_metadata, input_metadata, field_name) do
    Enum.reduce(
      input_metadata,
      processed_metadata,
      fn
        {key, value}, acc ->
          cond do
            field_name == key ->
              Map.put(acc, key, value)

            # for instance `client_name#fr`
            String.starts_with?(key, field_name <> "#") ->
              field_name_i18n = field_name <> "_i18n"

              [^field_name, i18n_key] = String.split(key, "#")

              case acc[field_name_i18n] do
                nil ->
                  Map.put(acc, field_name_i18n, %{i18n_key => value})

                # key already exsists
                _ ->
                  put_in(acc, [field_name_i18n, i18n_key], value)
              end

            true ->
              acc
          end
      end
    )
  end
end


<|start_filename|>lib/asteroid/object_store/authenticated_session/riak.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticatedSession.Riak do
  @moduledoc """
  Riak implementation of the `Asteroid.ObjectStore.AuthenticatedSession` behaviour

  ## Initializing a Riak bucket type

  ```console
  $ sudo riak-admin bucket-type create ephemeral_token '{"props":{"datatype":"map", "backend":"leveldb_mult"}}'
  ephemeral_token created

  $ sudo riak-admin bucket-type activate ephemeral_token
  ephemeral_token has been activated
  ```

  ## Options
  The options (`Asteroid.ObjectStore.AuthenticatedSession.opts()`) are:
  - `:bucket_type`: an `String.t()` for the bucket type that must be created beforehand in
  Riak. No defaults, **mandatory**
  - `bucket_name`: a `String.t()` for the bucket name. Defaults to `"authenticated_session"`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `1800` (30 minutes)
  - `:rows`: the maximum number of results that a search will return. Defaults to `1_000_000`.
  Search is used by the purge process.

  ## Installation function

  The `install/1` function executes the following actions:
  - it installs a custom schema (`asteroid_object_store_authenticated_session_riak_schema`)
  - it creates a new index (`asteroid_object_store_authenticated_session_riak_index`) on the bucket
  (and not the bucket type - so as to avoid collisions)

  This is necessary to:
  1. Efficiently index expiration timestamp
  2. Disable indexing of raw authenticated session data

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Riak).
  """

  require Logger

  @behaviour Asteroid.ObjectStore.AuthenticatedSession

  @impl true

  def install(opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authenticated_session"

    with :ok <-
           Riak.Search.Schema.create(
             schema_name(),
             (:code.priv_dir(:asteroid) ++ '/riak/object_store_authenticated_session_schema.xml')
             |> File.read!()
           ),
         :ok <- Riak.Search.Index.put(index_name(), schema_name()),
         :ok <- Riak.Search.Index.set({bucket_type, bucket_name}, index_name()) do
      Logger.info(
        "#{__MODULE__}: created authenticated session store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}`"
      )

      :ok
    else
      e ->
        "#{__MODULE__}: failed to create authenticated session store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

        {:error, "#{inspect(e)}"}
    end
  catch
    :exit, e ->
      bucket_type = opts[:bucket_type] || raise "Missing bucket type"
      bucket_name = opts[:bucket_name] || "authenticated_session"

      "#{__MODULE__}: failed to create authenticated session store `#{bucket_name}` " <>
        "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

      {:error, "#{inspect(e)}"}
  end

  @impl true

  def start_link(opts) do
    opts = Keyword.merge([purge_interval: 1800], opts)

    # we launch the process anyway because we need to return a process
    # but the singleton will do nothing if the value is `:no_purge`
    Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)
  end

  @impl true

  def get(authenticated_session_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authenticated_session"

    case Riak.find(bucket_type, bucket_name, authenticated_session_id) do
      res when not is_nil(res) ->
        authenticated_session =
          res
          |> Riak.CRDT.Map.get(:register, "authenticated_session_data_binary")
          |> Base.decode64!(padding: false)
          |> :erlang.binary_to_term()

        Logger.debug(
          "#{__MODULE__}: getting authenticated session `#{authenticated_session_id}`, " <>
            "value: `#{inspect(authenticated_session)}`"
        )

        {:ok, authenticated_session}

      nil ->
        Logger.debug(
          "#{__MODULE__}: getting authenticated session `#{authenticated_session_id}`, " <>
            "value: `nil`"
        )

        {:ok, nil}
    end
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def get_from_subject_id(subject_id, opts) do
    search("subject_id_register:\"#{String.replace(subject_id, "\"", "\\\"")}\"", opts)
  end

  @impl true

  def put(authenticated_session, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authenticated_session"

    riak_map = Riak.CRDT.Map.new()

    authenticated_session_data_binary =
      authenticated_session
      |> :erlang.term_to_binary()
      |> Base.encode64(padding: false)
      |> Riak.CRDT.Register.new()

    riak_map =
      Riak.CRDT.Map.put(
        riak_map,
        "authenticated_session_data_binary",
        authenticated_session_data_binary
      )

    riak_map =
      if authenticated_session.subject_id != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "subject_id",
          Riak.CRDT.Register.new(authenticated_session.subject_id)
        )
      else
        riak_map
      end

    riak_map =
      if authenticated_session.data["exp"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "exp_int",
          Riak.CRDT.Register.new(to_string(authenticated_session.data["exp"]))
        )
      else
        Logger.warn(
          "Inserting authenticated session with no expiration: #{
            String.slice(authenticated_session.id, 1..5)
          }..."
        )

        riak_map
      end

    Riak.update(riak_map, bucket_type, bucket_name, authenticated_session.id)

    Logger.debug(
      "#{__MODULE__}: stored authenticated session `#{authenticated_session.id}`, " <>
        "value: `#{inspect(authenticated_session)}`"
    )

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def delete(authenticated_session_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authenticated_session"

    Riak.delete(bucket_type, bucket_name, authenticated_session_id)

    Logger.debug("#{__MODULE__}: deleted authenticated session `#{authenticated_session_id}`")

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @doc """
  Searches in Riak-stored authenticated sessions

  This function is used internaly and made available for user convenience. authenticated sessions are
  stored in the following fields:

  |               Field name                   |  Indexed as   |
  |--------------------------------------------|:-------------:|
  | authenticated_session_data_binary_register | *not indexed* |
  | subject_id                                 | string        |
  | exp_int_register                           | int           |

  Note that you are responsible for escaping values accordingly with Solr escaping.
  """

  @spec search(String.t(), Asteroid.ObjectStore.AuthenticatedSession.opts()) ::
          {:ok, [Asteroid.OIDC.AuthenticatedSession.id()]}
          | {:error, any()}

  def search(search_query, opts) do
    case Riak.Search.query(index_name(), search_query, rows: opts[:rows] || 1_000_000) do
      {:ok, {:search_results, result_list, _, _}} ->
        {:ok,
         for {_index_name, attribute_list} <- result_list do
           :proplists.get_value("_yz_rk", attribute_list)
         end}

      {:error, _} = error ->
        error
    end
  end

  @spec schema_name() :: String.t()

  defp schema_name(), do: "asteroid_object_store_authenticated_session_riak_schema"

  @doc false

  @spec index_name() :: String.t()

  def index_name(), do: "asteroid_object_store_authenticated_session_riak_index"
end


<|start_filename|>lib/asteroid/crypto/cache/ets.ex<|end_filename|>
defmodule Asteroid.Crypto.Key.Cache.ETS do
  @moduledoc """
  ETS cache for keys

  This cache implementation stores the keys in an ETS owned by the `#{__MODULE__}` process that
  is created by the `start_link/1` function (and is therefore supervised).

  This cache is local to an EVM instance.

  ## Security considerations

  - The ETS table is public, and can be accessed and modified by any process. Beware if you
  have some user code running
  - The ETS table is not dumped into crash dumps
  - The ETS table is in-memory - its content is never written to disk
  """

  @table_name :asteroid_jwk_cache

  use GenServer

  alias Asteroid.Crypto

  @behaviour Crypto.Key.Cache

  @impl GenServer

  def init(_) do
    :ets.new(@table_name, [:named_table, :public])

    {:ok, []}
  end

  @impl Crypto.Key.Cache

  def start_link(_opts) do
    GenServer.start_link(__MODULE__, [], name: __MODULE__)
  end

  @impl Crypto.Key.Cache

  def get(key_name, _opts) do
    case :ets.lookup(@table_name, key_name) do
      [{_, key}] ->
        {:ok, key}

      _ ->
        {:error, Crypto.Key.Cache.NotFoundError.exception([])}
    end
  end

  @impl Crypto.Key.Cache

  def get_all(_opts) do
    for [key_name, key] <- :ets.match(@table_name, {:"$0", :"$1"}) do
      {key_name, key}
    end
  rescue
    e ->
      {:error, e}
  end

  @impl Crypto.Key.Cache

  def put(key_name, key, _opts) do
    :ets.insert(@table_name, {key_name, key})

    :ok
  rescue
    e ->
      {:error, e}
  end

  @impl Crypto.Key.Cache

  def delete(key_name, _opts) do
    :ets.delete(@table_name, key_name)

    :ok
  end
end


<|start_filename|>config/test.exs<|end_filename|>
use Mix.Config

# We don't run a server during test. If one is required,
# you can enable the server option below.
config :asteroid, AsteroidWeb.Endpoint,
  http: [port: 4000],
  server: false

# Print only warnings and errors during test
config :logger, level: :warn

# Configure your database
config :asteroid, Asteroid.Repo,
  username: "postgres",
  password: "<PASSWORD>",
  database: "asteroid_test",
  hostname: "localhost",
  pool: Ecto.Adapters.SQL.Sandbox

config :mnesia,
  dir: 'Mnesia.#{node()}-#{Mix.env()}'

config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}

# Configure your database
config :asteroid, Asteroid.Repo,
  username: "postgres",
  password: "<PASSWORD>",
  database: "asteroid_dev",
  hostname: "localhost",
  pool_size: 10

######################################################################
######################################################################
################## Asteroid configuration ############################
######################################################################
######################################################################

config :asteroid, :object_store_access_token,
  module: Asteroid.ObjectStore.AccessToken.Mnesia,
  opts: [tab_def: [disc_copies: []]]

config :asteroid, :object_store_refresh_token,
  module: Asteroid.ObjectStore.RefreshToken.Mnesia,
  opts: [tab_def: [disc_copies: []]]

config :asteroid, :object_store_authorization_code,
  module: Asteroid.ObjectStore.AuthorizationCode.Mnesia

config :asteroid, :object_store_device_code, module: Asteroid.ObjectStore.DeviceCode.Mnesia

config :asteroid, :object_store_request_object,
  module: Asteroid.ObjectStore.GenericKV.Mnesia,
  opts: [table_name: :request_object]

config :asteroid, :attribute_repositories,
  subject: [
    module: AttributeRepositoryMnesia,
    run_opts: [instance: :subject],
    init_opts: [instance: :subject]
  ],
  client: [
    module: AttributeRepositoryMnesia,
    run_opts: [instance: :client],
    init_opts: [instance: :client]
  ],
  device: [
    module: AttributeRepositoryMnesia,
    run_opts: [instance: :device],
    init_opts: [instance: :device]
  ]

config :asteroid, :api_oauth2_plugs, [
  {APIacFilterIPWhitelist, [whitelist: ["127.0.0.1/32"], error_response_verbosity: :debug]}
]

config :asteroid, :api_oauth2_endpoint_token_plugs, [
  {Corsica, [origins: "*"]},
  {APIacAuthBasic,
   realm: "always erroneous client password",
   callback: &Asteroid.Utils.always_nil/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {
     APIacAuthBearer.Validator.Identity,
     [response: {:error, :invalid_token}]
   },
   set_error_response: &APIacAuthBearer.save_authentication_failure_response/3,
   error_response_verbosity: :debug}
]

config :asteroid, :api_oauth2_endpoint_introspect_plugs, [
  {APIacAuthBasic,
   realm: "always erroneous client password",
   callback: &Asteroid.Utils.always_nil/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {Asteroid.OAuth2.APIacAuthBearer.Validator, []},
   set_error_response: &APIacAuthBearer.save_authentication_failure_response/3,
   error_response_verbosity: :debug,
   required_scopes: ["asteroid.introspect"],
   forward_metadata: ["scope"]}
]

config :asteroid, :api_oauth2_endpoint_revoke_plugs, [
  {Corsica, [origins: "*"]},
  {APIacAuthBasic,
   realm: "always erroneous client password",
   callback: &Asteroid.Utils.always_nil/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {
     APIacAuthBearer.Validator.Identity,
     [response: {:error, :invalid_token}]
   },
   set_error_response: &APIacAuthBearer.save_authentication_failure_response/3,
   error_response_verbosity: :debug}
]

config :asteroid, :api_oauth2_endpoint_register_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {Asteroid.OAuth2.APIacAuthBearer.Validator, []},
   set_error_response: &APIacAuthBearer.save_authentication_failure_response/3,
   error_response_verbosity: :debug,
   required_scopes: ["asteroid.register"],
   forward_metadata: ["scope"]}
]

config :asteroid, :api_oauth2_endpoint_device_authorization_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
   error_response_verbosity: :debug}
]

config :asteroid, :oauth2_grant_types_enabled, [
  :authorization_code,
  :implicit,
  :password,
  :client_credentials,
  :refresh_token,
  :"urn:ietf:params:oauth:grant-type:device_code"
]

config :asteroid, :api_oidc_plugs, []

config :asteroid, :api_oidc_endpoint_userinfo_plugs, [
  {Corsica, [origins: "*"]},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {Asteroid.OAuth2.APIacAuthBearer.Validator, []},
   bearer_extract_methods: [:header, :body],
   forward_bearer: true,
   error_response_verbosity: :normal}
]

config :asteroid, :oauth2_response_types_enabled, [
  :code,
  :token,
  :id_token,
  :"id_token token",
  :"code id_token",
  :"code token",
  :"code id_token token"
]

config :asteroid, :oauth2_response_mode_policy, :oidc_only

config :asteroid, :api_error_response_verbosity, :debug

config :asteroid,
       :oauth2_flow_ropc_username_password_verify_callback,
       &Asteroid.Test.Callbacks.test_ropc_username_password_callback/3

config :asteroid, :oauth2_scope_callback, &Asteroid.OAuth2.Scope.grant_for_flow/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_password_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_password_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_refresh_token_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_refresh_token_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# Endpoint: introspect

config :asteroid,
       :oauth2_endpoint_introspect_client_authorized,
       &Asteroid.OAuth2.Client.endpoint_introspect_authorized?/1

config :asteroid, :oauth2_endpoint_introspect_claims_resp, [
  "scope",
  "client_id",
  "username",
  "token_type",
  "exp",
  "iat",
  "nbf",
  "sub",
  "aud",
  "iss",
  "jti"
]

config :asteroid,
       :oauth2_endpoint_introspect_claims_resp_callback,
       &Asteroid.OAuth2.Introspect.endpoint_introspect_claims_resp/1

config :asteroid,
       :oauth2_endpoint_introspect_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_introspect_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# Endpoint: revoke

config :asteroid,
       :oauth2_endpoint_revoke_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# Flow: client credentials

config :asteroid, :oauth2_flow_client_credentials_issue_refresh_token_init, false

config :asteroid, :oauth2_flow_client_credentials_issue_refresh_token_refresh, false

config :asteroid, :oauth2_flow_client_credentials_access_token_lifetime, 60 * 10

config :asteroid,
       :oauth2_endpoint_token_grant_type_client_credentials_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_client_credentials_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# Refresh tokens

config :asteroid,
       :object_store_refresh_token_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_issue_refresh_token_callback,
       &Asteroid.Token.RefreshToken.issue_refresh_token?/1

config :asteroid, :oauth2_flow_ropc_issue_refresh_token_init, true

config :asteroid, :oauth2_flow_ropc_issue_refresh_token_refresh, false

config :asteroid, :oauth2_refresh_token_lifetime_callback, &Asteroid.Token.RefreshToken.lifetime/1

# 1 week
config :asteroid, :oauth2_flow_ropc_refresh_token_lifetime, 60 * 60 * 24 * 7

# access tokens

config :asteroid,
       :object_store_access_token_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oauth2_access_token_lifetime_callback, &Asteroid.Token.AccessToken.lifetime/1

config :asteroid, :oauth2_flow_ropc_access_token_lifetime, 60 * 10

config :asteroid, :client_credentials_issue_refresh_token, false

config :asteroid, :client_credentials_scope_callback, &Asteroid.Utils.id_first_param/2

# authorization codes

config :asteroid,
       :object_store_authorization_code_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_authorization_code_lifetime_callback,
       &Asteroid.Token.AuthorizationCode.lifetime/1

config :asteroid, :oauth2_flow_authorization_code_authorization_code_lifetime, 60

config :asteroid,
       :oauth2_endpoint_authorize_before_send_redirect_uri_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_authorize_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oauth2_flow_authorization_code_issue_refresh_token_init, true

config :asteroid, :oauth2_flow_authorization_code_issue_refresh_token_refresh, false

config :asteroid,
       :oauth2_flow_authorization_code_refresh_token_lifetime,
       # 1 week
       60 * 60 * 24 * 7

config :asteroid, :oauth2_flow_authorization_code_access_token_lifetime, 60 * 10

config :asteroid,
       :oauth2_endpoint_token_grant_type_authorization_code_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_authorization_code_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oauth2_pkce_policy, :optional

config :asteroid, :oauth2_pkce_allowed_methods, [:plain, :S256]

config :asteroid, :oauth2_pkce_must_use_callback, &Asteroid.OAuth2.Client.must_use_pkce?/1

# implicit flow

config :asteroid, :oauth2_flow_implicit_access_token_lifetime, 60 * 60

# client registration

config :asteroid,
       :oauth2_endpoint_register_authorization_callback,
       &Asteroid.OAuth2.Register.request_authorized?/2

config :asteroid, :oauth2_endpoint_register_authorization_policy, :authorized_clients

config :asteroid, :oauth2_endpoint_register_additional_metadata_field, ["test_field"]

config :asteroid,
       :oauth2_endpoint_register_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_client_before_save_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_gen_client_id_callback,
       &Asteroid.OAuth2.Register.generate_client_id/2

config :asteroid,
       :oauth2_endpoint_register_gen_client_resource_id_callback,
       &Asteroid.OAuth2.Register.generate_client_resource_id/2

config :asteroid,
       :oauth2_endpoint_register_client_type_callback,
       &Asteroid.OAuth2.Register.client_type/1

# endpoint token

config :asteroid,
       :oauth2_endpoint_token_auth_methods_supported_callback,
       &Asteroid.OAuth2.Endpoint.token_endpoint_auth_methods_supported/0

# scope configuration

config :asteroid, :scope_config,
  scopes: %{
    "scp1" => [],
    "scp2" => [],
    "scp3" => [],
    "scp4" => [],
    "scp5" => [],
    "scp6" => [],
    "openid" => []
  }

# OAuth2 metadata

config :asteroid, :oauth2_endpoint_metadata_before_send_resp_callback, &Asteroid.Utils.id/1

config :asteroid, :oauth2_endpoint_metadata_before_send_conn_callback, &Asteroid.Utils.id/1

# JWKs URI

config :asteroid, :oauth2_endpoint_discovery_keys_before_send_resp_callback, &Asteroid.Utils.id/1

config :asteroid, :oauth2_endpoint_discovery_keys_before_send_conn_callback, &Asteroid.Utils.id/1

# crypto

config :asteroid, :crypto_keys, %{
  "key_auto_sig" => {:auto_gen, [params: {:rsa, 1024}, use: :sig]},
  "key_auto_enc" => {:auto_gen, [params: {:rsa, 1024}, use: :enc]}
}

config :asteroid, :crypto_keys_cache, {Asteroid.Crypto.Key.Cache.ETS, []}

# JWS access tokens

config :asteroid,
       :oauth2_access_token_serialization_format_callback,
       &Asteroid.Token.AccessToken.serialization_format/1

config :asteroid,
       :oauth2_access_token_signing_key_callback,
       &Asteroid.Token.AccessToken.signing_key/1

config :asteroid,
       :oauth2_access_token_signing_alg_callback,
       &Asteroid.Token.AccessToken.signing_alg/1

config :asteroid, :oauth2_flow_ropc_access_token_serialization_format, :opaque

config :asteroid, :oauth2_flow_ropc_access_token_signing_key, "key_auto"

config :asteroid, :oauth2_flow_ropc_access_token_signing_alg, "RS384"
#
# device authorization flow

config :asteroid,
       :oauth2_endpoint_device_authorization_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_device_authorization_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_device_code_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oauth2_flow_device_authorization_device_code_lifetime, 60 * 15

config :asteroid,
       :oauth2_flow_device_authorization_user_code_callback,
       &Asteroid.OAuth2.DeviceAuthorization.user_code/1

config :asteroid, :oauth2_flow_device_authorization_issue_refresh_token_init, true

config :asteroid, :oauth2_flow_device_authorization_issue_refresh_token_refresh, false

config :asteroid, :oauth2_flow_device_authorization_refresh_token_lifetime, 10 * 365 * 24 * 3600

config :asteroid, :oauth2_flow_device_authorization_access_token_lifetime, 60 * 10

config :asteroid,
       :oauth2_endpoint_token_grant_type_device_code_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_device_code_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_flow_device_authorization_rate_limiter,
       {Asteroid.OAuth2.DeviceAuthorization.RateLimiter.Hammer, []}

config :asteroid, :oauth2_flow_device_authorization_rate_limiter_interval, 5

config :asteroid, :web_authorization_callback, &Asteroid.WebFlow.web_authorization_callback/2

config :asteroid, :oidc_id_token_lifetime_callback, &Asteroid.Token.IDToken.lifetime/1

config :asteroid, :oidc_id_token_signing_key_callback, &Asteroid.Token.IDToken.signing_key/1

config :asteroid, :oidc_id_token_signing_alg_callback, &Asteroid.Token.IDToken.signing_alg/1

config :asteroid, :token_id_token_before_serialize_callback, &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oidc_issue_id_token_on_refresh_callback,
       &Asteroid.Token.IDToken.issue_id_token?/1

config :asteroid, :oidc_flow_authorization_code_authorization_code_lifetime, 60

config :asteroid,
       :oidc_endpoint_userinfo_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oidc_endpoint_userinfo_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oidc_id_token_encrypt_callback, &Asteroid.Token.IDToken.encrypt_token?/1

config :asteroid, :oidc_subject_identifier_callback, &Asteroid.OIDC.subject_identifier/2

config :asteroid,
       :oidc_subject_identifier_pairwise_salt,
       Base.encode64(:crypto.strong_rand_bytes(24))

config :asteroid, :object_store_authenticated_session,
  module: Asteroid.ObjectStore.AuthenticatedSession.Mnesia

config :asteroid, :object_store_authentication_event,
  module: Asteroid.ObjectStore.AuthenticationEvent.Mnesia

config :asteroid,
       :object_store_authenticated_session_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authentication_event_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oidc_acr_config,
  loa2: [
    callback: &Asteroid.Test.Callbacks.authorize_print_successful_request/2,
    auth_event_set: [["password", "otp"], ["password", "<PASSWORD>"], ["webauthn", "otp"]]
  ],
  loa1: [
    callback: &Asteroid.Test.Callbacks.authorize_print_successful_request/2,
    auth_event_set: [["password"], ["webauthn"], ["otp"]]
  ]


<|start_filename|>lib/asteroid_web/controllers/api/oauth2/revoke_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.RevokeController do
  @moduledoc false

  use AsteroidWeb, :controller
  import Asteroid.Utils
  alias Asteroid.Token.{RefreshToken, AccessToken}
  alias Asteroid.Client
  alias Asteroid.OAuth2

  def handle(conn, %{"token" => token} = params) do
    with {:ok, client} <- OAuth2.Client.get_client(conn),
         :ok <- valid_token_parameter?(token, params["token_type_hint"]) do
      client = Client.fetch_attributes(client, ["client_id"])

      # according to the specification, an error message should be returned if the token was
      # not issued to the requesting client
      # However, as it could leak token info, Asteroid returns 200 OK instead
      # see also: https://security.stackexchange.com/questions/210609/oauth2-what-to-return-when-revoking-a-token-which-is-not-the-clients-rfc7009

      case params["token_type_hint"] do
        token_type_hint when token_type_hint in [nil, "access_token"] ->
          case AccessToken.get(token) do
            {:ok, access_token} ->
              if access_token.data["client_id"] == client.attrs["client_id"] do
                AccessToken.delete(token)
              end

            {:error, _} ->
              case RefreshToken.get(token) do
                {:ok, refresh_token} ->
                  if refresh_token.data["client_id"] == client.attrs["client_id"] do
                    RefreshToken.delete(token)
                  end

                _ ->
                  :ok
              end
          end

          success_response(conn, client)

        "refresh_token" ->
          case RefreshToken.get(token) do
            {:ok, refresh_token} ->
              if refresh_token.data["client_id"] == client.attrs["client_id"] do
                RefreshToken.delete(token)
              end

            {:error, _} ->
              case AccessToken.get(token) do
                {:ok, access_token} ->
                  if access_token.data["client_id"] == client.attrs["client_id"] do
                    AccessToken.delete(token)
                  end

                _ ->
                  :ok
              end
          end

          success_response(conn, client)

        _ ->
          error_resp(conn, 400,
            error: :unsupported_token_type,
            error_description: "Unrecognized `token_type_hint`"
          )
      end
    else
      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, _) do
    error_resp(conn, 400,
      error: :invalid_request,
      error_description: "Missing `token` parameter"
    )
  end

  @spec valid_token_parameter?(String.t(), String.t() | nil) ::
          :ok
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp valid_token_parameter?(token, "access_token") do
    if OAuth2Utils.valid_access_token_param?(token) do
      :ok
    else
      {:error, OAuth2.Request.MalformedParamError.exception(name: "token", value: token)}
    end
  end

  defp valid_token_parameter?(token, "refresh_token") do
    if OAuth2Utils.valid_refresh_token_param?(token) do
      :ok
    else
      {:error, OAuth2.Request.MalformedParamError.exception(name: "token", value: token)}
    end
  end

  defp valid_token_parameter?(token, _) do
    if OAuth2Utils.valid_access_token_param?(token) or
         OAuth2Utils.valid_refresh_token_param?(token) do
      :ok
    else
      {:error, OAuth2.Request.MalformedParamError.exception(name: "token", value: token)}
    end
  end

  @spec success_response(Plug.Conn.t(), Client.t()) :: Plug.Conn.t()

  defp success_response(conn, client) do
    ctx =
      %{}
      |> Map.put(:endpoint, :revoke)
      |> Map.put(:client, client)
      |> Map.put(:conn, conn)

    conn
    |> astrenv(:oauth2_endpoint_revoke_before_send_conn_callback).(ctx)
    |> Plug.Conn.resp(200, [])
  end

  defp error_resp(conn, error_status, error_data) do
    conn
    |> put_status(error_status)
    |> json(Enum.into(error_data, %{}))
  end
end


<|start_filename|>lib/asteroid_web/controllers/authorize_controller.ex<|end_filename|>
defmodule AsteroidWeb.AuthorizeController do
  use AsteroidWeb, :controller

  require Logger

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias Asteroid.OAuth2
  alias Asteroid.OIDC
  alias Asteroid.OIDC.AuthenticatedSession
  alias Asteroid.Context
  alias Asteroid.Client
  alias Asteroid.Subject
  alias Asteroid.Token.{AccessToken, AuthorizationCode, IDToken}

  defmodule Request do
    @moduledoc """
    Struct with the necessary information to process an web authorization request
    """

    @enforce_keys [
      :flow,
      :response_type,
      :response_mode,
      :client_id,
      :redirect_uri,
      :requested_scopes,
      :params
    ]

    defstruct [
      :flow,
      :response_type,
      :response_mode,
      :client_id,
      :redirect_uri,
      :requested_scopes,
      :pkce_code_challenge,
      :pkce_code_challenge_method,
      :nonce,
      :display,
      :prompt,
      :max_age,
      :ui_locales,
      :id_token_hint,
      :login_hint,
      :acr_values,
      :preferred_acr,
      :claims,
      :params
    ]

    @type t :: %__MODULE__{
            flow: OAuth2.flow(),
            response_type: OAuth2.response_type(),
            response_mode: OAuth2.response_mode(),
            client_id: OAuth2.client_id(),
            redirect_uri: OAuth2.RedirectUri.t(),
            requested_scopes: Scope.Set.t(),
            pkce_code_challenge: OAuth2.PKCE.code_challenge() | nil,
            pkce_code_challenge_method: OAuth2.PKCE.code_challenge_method() | nil,
            nonce: String.t() | nil,
            display: String.t() | nil,
            prompt: [String.t()],
            max_age: non_neg_integer() | nil,
            ui_locales: [String.t()] | nil,
            id_token_hint: String.t() | nil,
            login_hint: String.t() | nil,
            acr_values: [Asteroid.OIDC.acr()] | nil,
            preferred_acr: Asteroid.OIDC.acr(),
            claims: map(),
            params: map()
          }
  end

  @type web_authorization_callback ::
          (Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t() ->
             {:ok, (Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t() -> Plug.Conn.t())}
             | {:error, Exception.t()})

  @doc false

  @spec pre_authorize(Plug.Conn.t(), map()) :: Plug.Conn.t()

  def pre_authorize(conn, %{"request" => _, "request_uri" => _}) do
    AsteroidWeb.Error.respond_authorize(
      conn,
      OAuth2.Request.InvalidRequestError.exception(
        reason: "`request` and `request_uri` parameters cannot be used simultaneously"
      )
    )
  end

  def pre_authorize(conn, %{"request" => _} = params) do
    case protocol(params) do
      :oauth2 ->
        jar_pre_authorize_oauth2(conn, params)

      :oidc ->
        jar_pre_authorize_oidc(conn, params)
    end
  end

  def pre_authorize(conn, %{"request_uri" => _} = params) do
    case protocol(params) do
      :oauth2 ->
        jar_pre_authorize_oauth2(conn, params)

      :oidc ->
        jar_pre_authorize_oidc(conn, params)
    end
  end

  def pre_authorize(
        conn,
        %{
          "response_type" => response_type_str,
          "client_id" => client_id,
          "redirect_uri" => redirect_uri
        } = params
      )
      when response_type_str in [
             "code",
             "token",
             "id_token",
             "id_token token",
             "code id_token",
             "code token",
             "code id_token token"
           ] do
    requested_scopes =
      case params["scope"] do
        nil ->
          Scope.Set.new()

        val ->
          Scope.Set.from_scope_param!(val)
      end

    protocol = if "openid" in requested_scopes, do: :oidc, else: :oauth2

    with {:ok, flow} <- OAuth2.response_type_to_flow(response_type_str, protocol),
         {:ok, response_type} <- Asteroid.OAuth2.to_response_type(response_type_str),
         :ok <- Asteroid.OAuth2.response_type_enabled?(response_type),
         :ok <- client_id_valid?(client_id),
         :ok <- redirect_uri_valid?(redirect_uri),
         {:ok, client} <- Client.load_from_unique_attribute("client_id", client_id),
         :ok <- redirect_uri_registered_for_client?(client, redirect_uri),
         :ok <- OAuth2.Client.response_type_authorized?(client, response_type_str),
         :ok <- OAuth2.Scope.scopes_enabled?(requested_scopes, flow),
         :ok <- OAuth2.Client.scopes_authorized?(client, requested_scopes),
         {:ok, {maybe_code_challenge, maybe_code_challenge_method}} <-
           maybe_pkce_params(client, params, flow),
         :ok <- nonce_parameter_present(params, flow),
         {:ok, response_mode} <- response_mode(params, flow),
         {:ok, claims_param} <- parse_claims_param(params) do
      client = Client.fetch_attributes(client, ["client_id"])

      case protocol do
        :oauth2 ->
          req = %Request{
            flow: flow,
            response_type: response_type,
            response_mode: response_mode,
            client_id: client.attrs["client_id"],
            redirect_uri: redirect_uri,
            requested_scopes: requested_scopes,
            pkce_code_challenge: maybe_code_challenge,
            pkce_code_challenge_method: maybe_code_challenge_method,
            params: params
          }

          case astrenv(:web_authorization_callback).(conn, req) do
            {:ok, callback} ->
              callback.(conn, req)

            {:error, e} ->
              AsteroidWeb.Error.respond_authorize(conn, e)
          end

        :oidc ->
          with :ok <- oidc_param_display_valid(params["display"]),
               {:ok, prompt_values} <- oidc_param_prompt_parse(params["prompt"]),
               {:ok, max_age_int} <- oidc_param_max_age_parse(params["max_age"]),
               {:ok, ui_locales_list} <- oidc_param_ui_locales_parse(params["ui_locales"]),
               {:ok, acr_values_list} <- oidc_param_acr_values_parse(params["acr_values"]),
               {:ok, preferred_acr} <- preferred_acr(claims_param, acr_values_list, client) do
            req = %Request{
              flow: flow,
              response_type: response_type,
              response_mode: response_mode,
              client_id: client.attrs["client_id"],
              redirect_uri: redirect_uri,
              requested_scopes: requested_scopes,
              pkce_code_challenge: maybe_code_challenge,
              pkce_code_challenge_method: maybe_code_challenge_method,
              nonce: params["nonce"],
              display: params["display"],
              prompt: prompt_values,
              max_age: max_age_int,
              ui_locales: ui_locales_list,
              id_token_hint: params["id_token_hint"],
              login_hint: params["login_hint"],
              acr_values: acr_values_list,
              preferred_acr: preferred_acr,
              claims: claims_param,
              params: params
            }

            case astrenv(:web_authorization_callback).(conn, req) do
              {:ok, callback} ->
                callback.(conn, req)

              {:error, e} ->
                AsteroidWeb.Error.respond_authorize(conn, e)
            end
          else
            {:error, e} ->
              AsteroidWeb.Error.respond_authorize(conn, e)
          end
      end
    else
      {:error, %OAuth2.Client.AuthorizationError{reason: :unauthorized_scope} = e} ->
        AsteroidWeb.Error.respond_authorize(
          conn,
          OAuth2.AccessDeniedError.exception(reason: Exception.message(e))
        )

      {:error, %AttributeRepository.Read.NotFoundError{} = e} ->
        AsteroidWeb.Error.respond_authorize(
          conn,
          OAuth2.Request.InvalidRequestError.exception(
            reason: Exception.message(e),
            parameter: "client_id"
          )
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_authorize(conn, e)
    end
  rescue
    _ in Scope.Set.InvalidScopeParam ->
      AsteroidWeb.Error.respond_authorize(
        conn,
        OAuth2.Request.MalformedParamError.exception(name: "scope", value: params["scope"])
      )
  end

  def pre_authorize(conn, %{"redirect_uri" => redirect_uri, "client_id" => client_id} = params) do
    with :ok <- client_id_valid?(client_id),
         :ok <- redirect_uri_valid?(redirect_uri),
         {:ok, client} <- Client.load_from_unique_attribute("client_id", client_id),
         :ok <- redirect_uri_registered_for_client?(client, redirect_uri) do
      if params["response_type"] do
        AsteroidWeb.Error.respond_authorize(
          conn,
          OAuth2.UnsupportedResponseTypeError.exception(response_type: params["response_type"])
        )
      else
        AsteroidWeb.Error.respond_authorize(
          conn,
          OAuth2.Request.InvalidRequestError.exception(
            reason: "missing parameter",
            parameter: "response_type"
          )
        )
      end
    else
      {:error, %AttributeRepository.Read.NotFoundError{} = e} ->
        AsteroidWeb.Error.respond_authorize(
          conn,
          OAuth2.Request.InvalidRequestError.exception(
            reason: Exception.message(e),
            parameter: "client_id"
          )
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_authorize(conn, e)
    end
  end

  def pre_authorize(conn, _params) do
    AsteroidWeb.Error.respond_authorize(
      conn,
      OAuth2.Request.InvalidRequestError.exception(
        reason: "missing parameter",
        parameter: "client_id"
      )
    )
  end

  @doc """
  Callback to be called when the authorization is granted, typically after an authentication and
  authorization (approving scopes) process, or in case an authentication already occured
  recently (cookie).

  The `opts` parameter is a `map()` whose keys are:
  - `:authz_request`: the initial `t:AsteroidWeb.AuthorizeController.Request.t/0` authorization
  request (**mandatory**)
  - `:subject`: the `t:Asteroid.Subject.t/0` of the user having approved the request
  (**mandatory**)
  - `:granted_scopes`: a `t:OAuth2Utils.Scope.Set.t/0` for the granted scope. If none was granted
  (because none were requested, or because user did not authorize them), an empty
  `t:OAuth2Utils.Scope.Set.t/0` must be set (**mandatory**)
  - `:authenticated_session_id`: a `t:Asteroid.OIDC.AuthenticatedSession.id/0` for the
  authenticated session of the user
  - `:acr`: acr used if `:authenticated_session_id` is not set
  - `:amr`: amr used if `:authenticated_session_id` is not set
  - `:auth_time`: authentication time used if `:authenticated_session_id` is not set
  """

  @spec authorization_granted(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()

  def authorization_granted(conn, opts) do
    session_info = session_info(opts)

    case opts[:authz_request].claims["id_token"]["acr"] do
      %{"essential" => true, "values" => acr_list} ->
        if session_info[:acr] in acr_list do
          do_authorization_granted(conn, opts, session_info)
        else
          Logger.debug(
            "#{__MODULE__}: authorization denied (#{inspect(opts[:authz_request])}) " <>
              "with reason: returned acr value doesn't match mandatory acr"
          )

          conn
          |> assign(:authz_request, opts[:authz_request])
          |> AsteroidWeb.Error.respond_authorize(
            OAuth2.AccessDeniedError.exception(
              reason: "requested essential acr condition not satisfied"
            )
          )
        end

      _ ->
        do_authorization_granted(conn, opts, session_info)
    end
  end

  @spec do_authorization_granted(Plug.Conn.t(), Plug.opts(), map()) :: Plug.Conn.t()

  defp do_authorization_granted(conn, opts, session_info) do
    authz_request = opts[:authz_request]

    {:ok, client} =
      Client.load_from_unique_attribute(
        "client_id",
        authz_request.client_id,
        attributes: ["client_id"]
      )

    subject = Subject.fetch_attributes(opts[:subject], ["sub"])

    ctx =
      %{}
      |> Map.put(:endpoint, :authorize)
      |> Map.put(:flow, authz_request.flow)
      |> Map.put(:response_type, authz_request.response_type)
      |> Map.put(:requested_scopes, authz_request.requested_scopes)
      |> Map.put(:granted_scopes, opts[:granted_scopes])
      |> Map.put(:client, client)
      |> Map.put(:subject, subject)
      |> Map.put(:flow_result, opts)
      |> Map.put(:conn, conn)

    granted_scopes = astrenv(:oauth2_scope_callback).(opts[:granted_scopes], ctx)

    maybe_authorization_code_serialized =
      if authz_request.response_type in [
           :code,
           :"code id_token",
           :"code token",
           :"code id_token token"
         ] do
        {:ok, authorization_code} =
          AuthorizationCode.gen_new()
          |> AuthorizationCode.put_value("iat", now())
          |> AuthorizationCode.put_value(
            "exp",
            now() + astrenv(:oauth2_authorization_code_lifetime_callback).(ctx)
          )
          |> AuthorizationCode.put_value("client_id", client.attrs["client_id"])
          |> AuthorizationCode.put_value("redirect_uri", authz_request.redirect_uri)
          |> AuthorizationCode.put_value("sub", subject.attrs["sub"])
          |> AuthorizationCode.put_value(
            "requested_scopes",
            Scope.Set.to_list(authz_request.requested_scopes)
          )
          |> AuthorizationCode.put_value("granted_scopes", Scope.Set.to_list(granted_scopes))
          |> AuthorizationCode.put_value(
            "__asteroid_oauth2_initial_flow",
            Atom.to_string(authz_request.flow)
          )
          |> AuthorizationCode.put_value("iss", OAuth2.issuer())
          |> AuthorizationCode.put_value(
            "__asteroid_oauth2_pkce_code_challenge",
            authz_request.pkce_code_challenge
          )
          |> AuthorizationCode.put_value(
            "__asteroid_oauth2_pkce_code_challenge_method",
            if authz_request.pkce_code_challenge_method != nil do
              to_string(authz_request.pkce_code_challenge_method)
            end
          )
          |> AuthorizationCode.put_value("__asteroid_oidc_nonce", authz_request.nonce)
          |> AuthorizationCode.put_value(
            "__asteroid_oidc_authenticated_session_id",
            opts[:authenticated_session_id]
          )
          |> AuthorizationCode.put_value("__asteroid_oidc_initial_acr", session_info[:acr])
          |> AuthorizationCode.put_value("__asteroid_oidc_initial_amr", session_info[:amr])
          |> AuthorizationCode.put_value(
            "__asteroid_oidc_initial_auth_time",
            session_info[:auth_time]
          )
          |> AuthorizationCode.put_value("__asteroid_oidc_claims", authz_request.claims)
          |> AuthorizationCode.store(ctx)

        AuthorizationCode.serialize(authorization_code)
      end

    maybe_access_token =
      if authz_request.response_type in [
           :token,
           :"id_token token",
           :"code token",
           :"code id_token token"
         ] do
        {:ok, access_token} =
          new_access_token(ctx)
          |> AccessToken.put_value("iss", OAuth2.issuer())
          |> AccessToken.put_value("iat", now())
          |> AccessToken.put_value(
            "exp",
            now() + astrenv(:oauth2_access_token_lifetime_callback).(ctx)
          )
          |> AccessToken.put_value("client_id", client.attrs["client_id"])
          |> AccessToken.put_value("redirect_uri", authz_request.redirect_uri)
          |> AccessToken.put_value("sub", subject.attrs["sub"])
          |> AccessToken.put_value("scope", Scope.Set.to_list(granted_scopes))
          |> AccessToken.put_value(
            "__asteroid_oidc_authenticated_session_id",
            opts[:authenticated_session_id]
          )
          |> AccessToken.put_value(
            "__asteroid_oauth2_initial_flow",
            Atom.to_string(authz_request.flow)
          )
          |> AccessToken.put_value("__asteroid_oidc_claims", authz_request.claims)
          |> AccessToken.store(ctx)

        access_token
      end

    maybe_access_token_lifetime =
      if maybe_access_token do
        maybe_access_token.data["exp"] - maybe_access_token.data["iat"]
      end

    maybe_access_token_serialized =
      if maybe_access_token, do: AccessToken.serialize(maybe_access_token)

    maybe_id_token_serialized =
      if authz_request.response_type in [
           :id_token,
           :"id_token token",
           :"code id_token",
           :"code id_token token"
         ] do
        additional_claims = additional_claims(authz_request, granted_scopes)

        %IDToken{
          iss: OAuth2.issuer(),
          sub: astrenv(:oidc_subject_identifier_callback).(subject, client),
          aud: client.attrs["client_id"],
          exp: now() + astrenv(:oidc_id_token_lifetime_callback).(ctx),
          iat: now(),
          auth_time: session_info[:auth_time],
          acr: session_info[:acr],
          amr: session_info[:amr],
          nonce: authz_request.nonce,
          client: client,
          associated_access_token_serialized: maybe_access_token_serialized,
          associated_authorization_code_serialized: maybe_authorization_code_serialized
        }
        |> IDToken.add_sub_claims(additional_claims, subject)
        |> astrenv(:token_id_token_before_serialize_callback).(ctx)
        |> IDToken.serialize()
      else
        nil
      end

    params =
      %{}
      |> put_if_not_nil("code", maybe_authorization_code_serialized)
      |> put_if_not_nil("access_token", maybe_access_token_serialized)
      |> put_if_not_nil("id_token", maybe_id_token_serialized)
      |> put_if_not_nil("state", authz_request.params["state"])
      |> put_if_not_nil("token_type", if(maybe_access_token_serialized, do: "bearer"))
      |> put_if_not_nil("expires_in", maybe_access_token_lifetime)
      |> maybe_put_scope(
        authz_request.requested_scopes,
        opts[:granted_scopes],
        authz_request.response_type
      )

    case authz_request.response_mode do
      :query ->
        redirect_uri =
          authz_request.redirect_uri
          |> OAuth2.RedirectUri.add_params(params)
          |> astrenv(:oauth2_endpoint_authorize_before_send_redirect_uri_callback).(ctx)

        conn
        |> astrenv(:oauth2_endpoint_authorize_before_send_conn_callback).(ctx)
        |> redirect(external: redirect_uri)

      :fragment ->
        redirect_uri =
          authz_request.redirect_uri
          |> Kernel.<>("#")
          |> Kernel.<>(URI.encode_query(params))
          |> astrenv(:oauth2_endpoint_authorize_before_send_redirect_uri_callback).(ctx)

        conn
        |> astrenv(:oauth2_endpoint_authorize_before_send_conn_callback).(ctx)
        |> redirect(external: redirect_uri)

      :form_post ->
        conn
        |> put_status(200)
        |> put_resp_header("cache-control", "no-cache, no-store")
        |> put_resp_header("pragma", "no-cache")
        |> Plug.Conn.put_private(:plug_skip_csrf_protection, true)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.AuthorizeView)
        |> astrenv(:oauth2_endpoint_authorize_before_send_conn_callback).(ctx)
        |> render("authorization_form_post_response.html",
          params: params,
          target: authz_request.redirect_uri
        )
    end
  end

  @doc """
  Callback to be called when the authorization is denied, either by the user or by the
  server

  The options are a `map()` with the following keys (all **mandatory**):
  - `:authz_request`: the initial `t:AsteroidWeb.AuthorizeController.Request.t/0` authorization
  request
  - `:error`: one of the following exceptions:
    - `t:Asteroid.OAuth2.AccessDeniedError.t/0` when the request was denied either because of
    server
    policy or because of the user refusal
    - `t:Asteroid.OAuth2.ServerError.t/0` in case of server error
    - `t:Asteroid.OAuth2.TemporarilyUnavailableError.t/0` when the service is temporarily
    unavailable. Can be useful for maintenance mode
    - `t:Asteroid.OIDC.InteractionRequiredError.t/0`
    - `t:Asteroid.OIDC.LoginRequiredError.t/0`
    - `t:Asteroid.OIDC.AccountSelectionRequiredError.t/0`
    - `t:Asteroid.OIDC.ConsentRequiredError.t/0`
  """

  @spec authorization_denied(Plug.Conn.t(), Plug.opts()) :: Plug.Conn.t()

  def authorization_denied(conn, opts) do
    authz_request = opts[:authz_request]

    Logger.debug(
      "#{__MODULE__}: authorization denied (#{inspect(authz_request)}) with " <>
        "reason: `#{Exception.message(opts[:error])}`"
    )

    conn
    |> assign(:authz_request, authz_request)
    |> AsteroidWeb.Error.respond_authorize(opts[:error])
  end

  @spec jar_pre_authorize_oauth2(Plug.Conn.t(), map()) :: Plug.Conn.t()

  defp jar_pre_authorize_oauth2(conn, %{"request" => request_object} = params) do
    if astrenv(:oauth2_jar_enabled) in [:request_only, :enabled] do
      case OAuth2.JAR.verify_and_parse(request_object) do
        {:ok, jar_req_params} ->
          req_params = Map.merge(jar_delete_oauth2_request_parameters(params), jar_req_params)

          pre_authorize(conn, req_params)

        {:error, e} ->
          AsteroidWeb.Error.respond_authorize(conn, e)
      end
    else
      AsteroidWeb.Error.respond_authorize(
        conn,
        OAuth2.JAR.RequestNotSupportedError.exception([])
      )
    end
  end

  defp jar_pre_authorize_oauth2(conn, %{"request_uri" => request_uri} = params) do
    if astrenv(:oauth2_jar_enabled) in [:request_uri_only, :enabled] do
      with {:ok, jar_req_obj} <- OAuth2.JAR.retrieve_object(request_uri),
           {:ok, jar_req_params} = OAuth2.JAR.verify_and_parse(jar_req_obj) do
        req_params = Map.merge(jar_delete_oauth2_request_parameters(params), jar_req_params)

        pre_authorize(conn, req_params)
      else
        {:error, e} ->
          AsteroidWeb.Error.respond_authorize(conn, e)
      end
    else
      AsteroidWeb.Error.respond_authorize(
        conn,
        OAuth2.JAR.RequestURINotSupportedError.exception([])
      )
    end
  end

  @spec jar_pre_authorize_oidc(Plug.Conn.t(), map()) :: Plug.Conn.t()

  defp jar_pre_authorize_oidc(
         conn,
         %{
           "request" => request_object,
           "response_type" => response_type,
           "client_id" => client_id,
           "scope" => scope_param
         } = params
       ) do
    scopes = OAuth2Utils.Scope.Set.from_scope_param!(scope_param)

    if astrenv(:oauth2_jar_enabled) in [:request_only, :enabled] do
      case OAuth2.JAR.verify_and_parse(request_object) do
        {:ok, jar_req_params} ->
          if jar_req_params["response_type"] in [response_type, nil] and
               jar_req_params["client_id"] in [client_id, nil] and
               "openid" in scopes do
            req_params = Map.merge(Map.delete(params, "request"), jar_req_params)

            pre_authorize(conn, req_params)
          else
            AsteroidWeb.Error.respond_authorize(
              conn,
              OAuth2.JAR.InvalidRequestObjectError.exception(
                reason: "Request and request object `response_type` or `client_id` don't match"
              )
            )
          end

        {:error, e} ->
          AsteroidWeb.Error.respond_authorize(conn, e)
      end
    else
      AsteroidWeb.Error.respond_authorize(conn, OAuth2.JAR.RequestNotSupportedError.exception([]))
    end
  end

  defp jar_pre_authorize_oidc(
         conn,
         %{
           "request_uri" => request_uri,
           "response_type" => response_type,
           "client_id" => client_id,
           "scope" => scope_param
         } = params
       ) do
    scopes = OAuth2Utils.Scope.Set.from_scope_param!(scope_param)

    if astrenv(:oauth2_jar_enabled) in [:request_uri_only, :enabled] do
      with {:ok, jar_req_obj} <- OAuth2.JAR.retrieve_object(request_uri),
           {:ok, jar_req_params} = OAuth2.JAR.verify_and_parse(jar_req_obj) do
        if jar_req_params["response_type"] in [response_type, nil] and
             jar_req_params["client_id"] in [client_id, nil] and
             "openid" in scopes do
          req_params = Map.merge(Map.delete(params, "request_uri"), jar_req_params)

          pre_authorize(conn, req_params)
        else
          AsteroidWeb.Error.respond_authorize(
            conn,
            OAuth2.JAR.InvalidRequestObjectError.exception(
              reason: "Request and request object `response_type` or `client_id` don't match"
            )
          )
        end
      else
        {:error, e} ->
          AsteroidWeb.Error.respond_authorize(conn, e)
      end
    else
      AsteroidWeb.Error.respond_authorize(
        conn,
        OAuth2.JAR.RequestURINotSupportedError.exception([])
      )
    end
  end

  defp jar_pre_authorize_oidc(conn, params) do
    missing_parameter =
      if params["response_type"] == nil do
        "response_type"
      else
        if params["client_id"] == nil do
          "client_id"
        else
          "scope"
        end
      end

    AsteroidWeb.Error.respond_authorize(
      conn,
      OAuth2.Request.InvalidRequestError.exception(
        reason: "missing parameter",
        parameter: missing_parameter
      )
    )
  end

  @spec redirect_uri_registered_for_client?(Client.t(), OAuth2.RedirectUri.t()) ::
          :ok
          | {:error, %OAuth2.Request.InvalidRequestError{}}

  def redirect_uri_registered_for_client?(client, redirect_uri) do
    client = Client.fetch_attributes(client, ["redirect_uri"])

    if redirect_uri in (client.attrs["redirect_uris"] || []) do
      :ok
    else
      {:error,
       OAuth2.Request.InvalidRequestError.exception(
         reason: "unregistered `redirect_uri` for client",
         parameter: "redirect_uri"
       )}
    end
  end

  @spec protocol(map()) :: OAuth2.protocol()

  defp protocol(params) do
    case OAuth2Utils.Scope.Set.from_scope_param(params["scope"] || "") do
      {:ok, scopes} ->
        if "openid" in scopes, do: :oidc, else: :oauth2

      {:error, _} ->
        :oauth2
    end
  end

  @spec client_id_valid?(String.t()) ::
          :ok
          | {:error, %OAuth2.Request.MalformedParamError{}}

  def client_id_valid?(client_id) do
    if OAuth2Utils.valid_client_id_param?(client_id) do
      :ok
    else
      {:error, OAuth2.Request.MalformedParamError.exception(name: "client_id", value: client_id)}
    end
  end

  @spec redirect_uri_valid?(String.t()) ::
          :ok
          | {:error, %OAuth2.Request.MalformedParamError{}}

  def redirect_uri_valid?(redirect_uri) do
    if OAuth2.RedirectUri.valid?(redirect_uri) do
      :ok
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(
         name: "redirect_uri",
         value: redirect_uri
       )}
    end
  end

  @spec oidc_param_display_valid(String.t() | nil) ::
          :ok
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp oidc_param_display_valid(nil), do: :ok
  defp oidc_param_display_valid("page"), do: :ok
  defp oidc_param_display_valid("popup"), do: :ok
  defp oidc_param_display_valid("touch"), do: :ok
  defp oidc_param_display_valid("wap"), do: :ok

  defp oidc_param_display_valid(val),
    do: OAuth2.Request.MalformedParamError.exception(name: "display", value: val)

  @spec oidc_param_prompt_parse(String.t() | nil) ::
          {:ok, [String.t()]}
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp oidc_param_prompt_parse(nil) do
    {:ok, []}
  end

  defp oidc_param_prompt_parse(prompt_param) do
    prompt_values = String.split(prompt_param, " ")

    if Enum.all?(prompt_values, &(&1 in ["none", "login", "consent", "select_account"])) do
      {:ok, prompt_values}
    else
      {:error, OAuth2.Request.MalformedParamError.exception(name: "prompt", value: prompt_param)}
    end
  end

  @spec oidc_param_max_age_parse(String.t() | nil) ::
          {:ok, non_neg_integer() | nil}
          | {:error, %OAuth2.Request.MalformedParamError{}}

  def oidc_param_max_age_parse(nil) do
    {:ok, nil}
  end

  def oidc_param_max_age_parse(maybe_integer_str) do
    case Integer.parse(maybe_integer_str) do
      {max_age, _} ->
        {:ok, max_age}

      :error ->
        OAuth2.Request.MalformedParamError.exception(name: "max_age", value: maybe_integer_str)
    end
  end

  @spec oidc_param_ui_locales_parse(String.t() | nil) ::
          {:ok, [String.t()] | nil}
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp oidc_param_ui_locales_parse(nil) do
    {:ok, nil}
  end

  defp oidc_param_ui_locales_parse(ui_locales_param) do
    case String.split(ui_locales_param, " ") do
      [_ | _] = ui_locales_list ->
        {:ok, ui_locales_list}

      [] ->
        OAuth2.Request.MalformedParamError.exception(name: "ui_locales", value: "")
    end
  end

  @spec oidc_param_acr_values_parse(String.t() | nil) ::
          {:ok, [OIDC.acr()] | nil}
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp oidc_param_acr_values_parse(nil) do
    {:ok, nil}
  end

  defp oidc_param_acr_values_parse(acr_values_param) do
    case String.split(acr_values_param, " ") do
      [_ | _] = acr_values_list ->
        {:ok, acr_values_list}

      [] ->
        OAuth2.Request.MalformedParamError.exception(name: "acr_values", value: "")
    end
  end

  @spec preferred_acr(map() | nil, [OIDC.acr()] | nil, Client.t()) ::
          {:ok, OIDC.acr()}
          | {:ok, nil}
          | {:error, Exception.t()}

  defp preferred_acr(claims_param, acr_values, client) do
    case preferred_acr_from_claims_param(claims_param) do
      {:ok, acr} when is_binary(acr) ->
        {:ok, acr}

      {:error, _} = error ->
        error

      {:ok, nil} ->
        case preferred_acr_from_acr_values_param(acr_values) do
          {:ok, acr} when is_binary(acr) ->
            {:ok, acr}

          {:error, _} = error ->
            error

          {:ok, nil} ->
            acrs_config =
              Enum.map(astrenv(:oidc_acr_config, []), fn {k, _} -> Atom.to_string(k) end)

            client = Client.fetch_attributes(client, ["default_acr_values"])

            {:ok, Enum.find(client.attrs["default_acr_values"] || [], &(&1 in acrs_config))}
        end
    end
  end

  @spec preferred_acr_from_claims_param(map()) ::
          {:ok, OIDC.acr()}
          | {:ok, nil}
          | {:error, Exception.t()}

  defp preferred_acr_from_claims_param(nil) do
    {:ok, nil}
  end

  defp preferred_acr_from_claims_param(claims_param) do
    acrs_config = Enum.map(astrenv(:oidc_acr_config, []), fn {k, _} -> Atom.to_string(k) end)

    case claims_param["id_token"]["acr"] do
      %{"values" => [acr | _]} ->
        if acr in acrs_config do
          {:ok, acr}
        else
          {:error,
           OAuth2.Request.InvalidRequestError.exception(
             reason: "`acr` value of the `claims` parameter is unknown",
             parameter: "claims"
           )}
        end

      %{"value" => acr} when is_binary(acr) ->
        if acr in acrs_config do
          {:ok, acr}
        else
          {:error,
           OAuth2.Request.InvalidRequestError.exception(
             reason: "`acr` value of the `claims` parameter is unknown",
             parameter: "claims"
           )}
        end

      _ ->
        {:ok, nil}
    end
  end

  @spec preferred_acr_from_acr_values_param([OIDC.acr()]) ::
          {:ok, OIDC.acr()}
          | {:ok, nil}
          | {:error, Exception.t()}

  defp preferred_acr_from_acr_values_param(nil) do
    {:ok, nil}
  end

  defp preferred_acr_from_acr_values_param([acr | _]) do
    acrs_config = Enum.map(astrenv(:oidc_acr_config, []), fn {k, _} -> Atom.to_string(k) end)

    if acr in acrs_config do
      {:ok, acr}
    else
      {:error,
       OAuth2.Request.InvalidRequestError.exception(
         reason: "unknown acr requested",
         parameter: "acr_values"
       )}
    end
  end

  @spec nonce_parameter_present(map(), OAuth2.flow()) ::
          :ok
          | {:error, %OAuth2.Request.InvalidRequestError{}}

  defp nonce_parameter_present(%{"nonce" => _}, :oidc_implicit) do
    :ok
  end

  defp nonce_parameter_present(_, :oidc_implicit) do
    {:error,
     OAuth2.Request.InvalidRequestError.exception(reason: "missing parameter", parameter: "nonce")}
  end

  defp nonce_parameter_present(_, _) do
    :ok
  end

  @spec response_mode(map(), OAuth2.flow()) ::
          {:ok, OAuth2.response_mode()}
          | {:error, %OAuth2.Request.InvalidRequestError{}}

  defp response_mode(%{"response_mode" => response_mode_param}, flow) do
    if response_mode_param in ["query", "fragment", "form_post"] do
      response_mode =
        case response_mode_param do
          "query" ->
            :query

          "fragment" ->
            :fragment

          "form_post" ->
            :form_post
        end

      case astrenv(:oauth2_response_mode_policy, :oidc_only) do
        :disabled ->
          {:ok, OAuth2.default_response_mode(flow)}

        :oidc_only ->
          if flow in [:oidc_authorization_code, :oidc_implicit, :oidc_hybrid] do
            {:ok, response_mode}
          else
            {:ok, OAuth2.default_response_mode(flow)}
          end

        :enabled ->
          {:ok, response_mode}
      end
    else
      {:error,
       OAuth2.Request.InvalidRequestError.exception(
         parameter: "response_mode",
         reason: "unsupported value `#{response_mode_param}`"
       )}
    end
  end

  defp response_mode(_params, flow) do
    {:ok, OAuth2.default_response_mode(flow)}
  end

  @spec parse_claims_param(map()) :: {:ok, map() | nil} | {:error, Exception.t()}

  defp parse_claims_param(%{"claims" => claims_param}) do
    case Jason.decode(claims_param) do
      {:ok, %{} = claims} ->
        {:ok, claims}

      _ ->
        {:error,
         OAuth2.Request.MalformedParamError.exception(name: "claims", value: claims_param)}
    end
  end

  defp parse_claims_param(_) do
    {:ok, nil}
  end

  @spec maybe_put_scope(map(), Scope.Set.t(), Scope.Set.t(), OAuth2.response_type()) :: map()

  defp maybe_put_scope(m, requested_scopes, granted_scopes, response_type)
       when response_type in [:token, :"id_token token", :"code token", :"code id_token token"] do
    if Scope.Set.equal?(requested_scopes, granted_scopes) do
      m
    else
      Map.put(m, "scope", Enum.join(granted_scopes, " "))
    end
  end

  defp maybe_put_scope(m, _, _, _) do
    m
  end

  @spec maybe_pkce_params(Client.t(), map(), OAuth2.flow()) ::
          {:ok, {OAuth2.PKCE.code_challenge() | nil, OAuth2.PKCE.code_challenge_method() | nil}}
          | {:error, %OAuth2.Request.InvalidRequestError{}}
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp maybe_pkce_params(_client, _params, flow) when flow in [:implicit, :oidc_implicit] do
    {:ok, {nil, nil}}
  end

  defp maybe_pkce_params(client, params, flow)
       when flow in [
              :authorization_code,
              :oidc_authorization_code,
              :oidc_hybrid
            ] do
    case astrenv(:oauth2_pkce_policy, :optional) do
      :disabled ->
        {:ok, {nil, nil}}

      :optional ->
        if astrenv(:oauth2_pkce_must_use_callback).(client) do
          if params["code_challenge"] != nil do
            pkce_params_when_mandatory(params)
          else
            {:error,
             OAuth2.Request.InvalidRequestError.exception(
               parameter: "code_challenge",
               reason: "missing `code_challenge` parameter"
             )}
          end
        else
          if params["code_challenge"] != nil do
            pkce_params_when_mandatory(params)
          else
            {:ok, {nil, nil}}
          end
        end

      :mandatory ->
        pkce_params_when_mandatory(params)
    end
  end

  @spec pkce_params_when_mandatory(map()) ::
          {:ok, {OAuth2.PKCE.code_challenge(), OAuth2.PKCE.code_challenge_method()}}
          | {:error, %OAuth2.Request.InvalidRequestError{}}
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp pkce_params_when_mandatory(%{"code_challenge" => code_challenge} = params) do
    method =
      case params["code_challenge_method"] do
        method when is_binary(method) ->
          method

        nil ->
          "plain"
      end

    if OAuth2.PKCE.code_challenge_valid?(code_challenge) do
      case OAuth2.PKCE.code_challenge_method_from_string(method) do
        nil ->
          {:error,
           OAuth2.Request.InvalidRequestError.exception(
             reason: "unsupported code challenge method",
             parameter: "code_challenge_method"
           )}

        code_challenge_method when is_atom(code_challenge_method) ->
          if code_challenge_method in astrenv(:oauth2_pkce_allowed_methods) do
            {:ok, {code_challenge, code_challenge_method}}
          else
            {:error,
             OAuth2.Request.InvalidRequestError.exception(
               reason: "unsupported code challenge method",
               parameter: "code_challenge_method"
             )}
          end
      end
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(name: "code_challenge", value: code_challenge)}
    end
  end

  defp pkce_params_when_mandatory(_) do
    {:error,
     OAuth2.Request.InvalidRequestError.exception(
       parameter: "code_challenge",
       reason: "missing `code_challenge` parameter"
     )}
  end

  @spec new_access_token(Context.t(), Keyword.t()) :: AccessToken.t()

  defp new_access_token(ctx, access_token_opts \\ []) do
    serialization_format = astrenv(:oauth2_access_token_serialization_format_callback).(ctx)

    case serialization_format do
      :opaque ->
        AccessToken.gen_new(access_token_opts)

      :jws ->
        signing_key = astrenv(:oauth2_access_token_signing_key_callback).(ctx)
        signing_alg = astrenv(:oauth2_access_token_signing_alg_callback).(ctx)

        access_token_opts =
          access_token_opts
          |> Keyword.put(:serialization_format, serialization_format)
          |> Keyword.put(:signing_key, signing_key)
          |> Keyword.put(:signing_alg, signing_alg)

        AccessToken.gen_new(access_token_opts)
    end
  end

  # this function removes the OAuth2 request attributes as specified in the JAR specification:
  #    The Authorization Server MUST extract the set of Authorization
  #    Request parameters from the Request Object value.  The Authorization
  #    Server MUST only use the parameters in the Request Object even if the
  #    same parameter is provided in the query parameter.
  #
  # it also deletes any "request" and "request_uri" parameter

  @spec jar_delete_oauth2_request_parameters(map()) :: map()

  defp jar_delete_oauth2_request_parameters(params) do
    Enum.reduce(
      params,
      %{},
      fn
        {k, v}, acc ->
          standard_request_params =
            OAuth2Utils.get_parameters_for_location(:authorization_request, [:oauth2])

          if k not in standard_request_params do
            Map.put(acc, k, v)
          else
            acc
          end
      end
    )
    |> Map.delete("request")
    |> Map.delete("request_uri")
  end

  @spec session_info(map()) ::
          %{
            required(:acr) => Asteroid.OIDC.acr() | nil,
            required(:auth_time) => non_neg_integer() | nil,
            required(:amr) => [Asteroid.OIDC.amr(), ...]
          }
          | nil

  defp session_info(opts) do
    if opts[:authenticated_session_id] do
      AuthenticatedSession.info(opts[:authenticated_session_id])
    else
      %{
        acr: opts[:acr],
        amr: opts[:amr],
        auth_time: opts[:auth_time]
      }
    end
  end

  # The Claims requested by the profile, email, address, and phone scope values are returned
  # from the UserInfo Endpoint, as described in Section 5.3.2, when a response_type value is
  # used that results in an Access Token being issued. However, when no Access Token is issued
  # (which is the case for the response_type value id_token), the resulting Claims are returned
  # in the ID Token. 

  @spec additional_claims(Request.t(), Scope.Set.t()) :: [String.t()]

  defp additional_claims(authz_request, granted_scopes) do
    if authz_request.response_type == :id_token do
      Enum.reduce(
        OIDC.Userinfo.scope_claims_mapping(),
        [],
        fn
          {claim_scope, claim_values}, acc ->
            if claim_scope in granted_scopes do
              claim_values ++ acc
            else
              acc
            end
        end
      )
    else
      []
    end ++
      Map.keys(authz_request.claims["id_token"] || %{})
  end
end


<|start_filename|>lib/asteroid/object_store/generic_kv.ex<|end_filename|>
defmodule Asteroid.ObjectStore.GenericKV do
  @moduledoc """
  Behaviour for access token store
  """

  @type opts :: Keyword.t()

  @typedoc """
  Key of the object

  The key can be of any type. Implementation should carefully deal with non-string values.
  """

  @type key :: any()

  @type value :: any()

  @doc """
  Installs the access token store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the access token store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the access token store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an object from its id
  """

  @callback get(key(), opts()) ::
              {:ok, value() | nil}
              | {:error, any()}

  @doc """
  Stores an object

  If the object already exists, all of its data should be erased.

  If the object is a map, that contains the `"exp"` key, it can be used be the implementation
  to purge the object after that unix timestamp.
  """

  @callback put(key(), value(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an object
  """

  @callback delete(key(), opts()) :: :ok | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1
end


<|start_filename|>lib/asteroid/utils.ex<|end_filename|>
defmodule Asteroid.Utils do
  @doc """
  Returns the current UNIX timestamp
  """
  @spec now() :: non_neg_integer()
  def now(), do: System.system_time(:second)

  @doc """
  Returns a secure random base 64 string of `size` bytes of randomness
  """
  @spec secure_random_b64(non_neg_integer()) :: String.t()
  def secure_random_b64(bytes \\ 32) do
    :crypto.strong_rand_bytes(bytes)
    |> Base.url_encode64(padding: false)
  end

  @doc """
  Returns the configuration option value

  In tests, checks first the process dictionary for the value and fall backs to the standard
  configuration, so that one can set configuration at the testing process level using:

  ```elixir
  Process.put(:configuration_option, value)
  ```
  """
  @spec astrenv(atom()) :: any()

  if Mix.env() == :test do
    def astrenv(key, default_value \\ nil) do
      if key in Keyword.keys(Process.get()) do
        Process.get(key)
      else
        Application.get_env(:asteroid, key, default_value)
      end
    end
  else
    def astrenv(key, default_value \\ nil) do
      Application.get_env(:asteroid, key, default_value)
    end
  end

  @spec put_if_not_nil(map(), Map.key(), Map.value()) :: map()

  def put_if_not_nil(map, _, nil), do: map
  def put_if_not_nil(map, key, value), do: Map.put(map, key, value)

  @spec put_if_not_empty_string(map(), Map.key(), String.t()) :: map()

  def put_if_not_empty_string(map, _, ""), do: map
  def put_if_not_empty_string(map, key, value), do: Map.put(map, key, value)

  @spec put_if_not_empty(map(), Map.key(), [any()]) :: map()

  def put_if_not_empty(map, _, []), do: map
  def put_if_not_empty(map, key, list), do: Map.put(map, key, list)

  @doc """
  Returns the parameter unchanged
  """

  @spec id(any()) :: any()

  def id(param), do: param

  @doc """
  Returns the first parameter unchanged
  """

  @spec id_first_param(any(), any()) :: any()

  def id_first_param(param, _), do: param

  @doc """
  Always returns nil
  """

  @spec always_nil(any(), any()) :: nil

  def always_nil(_, _ \\ nil), do: nil

  @spec conn_not_authenticated?(Plug.Conn.t()) :: boolean()

  def conn_not_authenticated?(conn), do: not APIac.authenticated?(conn)

  @doc """
  Returns `true` if a list of headers contains, `false` otherwise

  ## Example

  ```elixir
  iex> headers
  [
    {"Date", "Sun, 28 Jul 2019 21:07:14 GMT"},
    {"Content-Type", "text/html;charset=utf-8"},
    {"Transfer-Encoding", "chunked"},
    {"Server", "Apache"},
    {"X-Powered-By", "PHP/5.6"},
    {"Vary", "Accept-Encoding"},
    {"Set-Cookie", "SERVERID100401=1520152|XT4Oh|XT4Oh; path=/"},
    {"Cache-control", "private"},
    {"X-IPLB-Instance", "28305"}
  ]
  iex> Asteroid.Utils.headers_contain_content_type?(headers, "text", "html")                      
  true
  iex> Asteroid.Utils.headers_contain_content_type?(headers, "application", "xml")
  false
  ```
  """

  @spec headers_contain_content_type?(list(), String.t(), String.t()) :: boolean()

  def headers_contain_content_type?(headers, type, subtype) do
    case Enum.find_value(
           headers,
           fn
             {header, value} ->
               if String.downcase(header) == "content-type" do
                 value
               else
                 false
               end
           end
         ) do
      nil ->
        false

      media_type ->
        case ContentType.content_type(media_type) do
          {:ok, ^type, ^subtype, _} ->
            true

          _ ->
            false
        end
    end
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oauth2/device_authorization_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.DeviceAuthorizationControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias AsteroidWeb.Router.Helpers, as: Routes
  alias Asteroid.Token.DeviceCode

  # error cases

  test "no credentials for confidential client", %{conn: conn} do
    req = %{}

    conn =
      conn
      |> post(Routes.device_authorization_path(conn, :handle), req)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "invalid basic credentials for confidential client", %{conn: conn} do
    req_body = %{}

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "invalid"))
      |> post(Routes.device_authorization_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "unknown public client", %{conn: conn} do
    req = %{"client_id" => "unkown_public_client"}

    conn =
      conn
      |> post(Routes.device_authorization_path(conn, :handle), req)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "malformed public client", %{conn: conn} do
    req = %{"client_id" => "unkown⌿public_client"}

    response =
      conn
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "malformed scope parameter", %{conn: conn} do
    req = %{
      "client_id" => "client_public_1",
      "scope" => "scp1 scp2 scpů"
    }

    response =
      conn
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(400)

    assert response["error"] == "invalid_scope"
  end

  test "unauthorized grant type", %{conn: conn} do
    req = %{"client_id" => "client_confidential_2"}

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_2", "<PASSWORD>"))
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(400)

    assert response["error"] == "unauthorized_client"
  end

  test "unauthorized scope", %{conn: conn} do
    req = %{
      "client_id" => "client_public_1",
      "scope" => "scp1 scp2 scp9"
    }

    response =
      conn
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(400)

    assert response["error"] == "invalid_scope"
  end

  # success cases

  test "successful request confidential client without scopes", %{conn: conn} do
    req = %{}

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(200)

    assert is_binary(response["device_code"])
    assert is_binary(response["user_code"])
    assert is_binary(response["verification_uri"])
    assert is_binary(response["verification_uri_complete"])
    assert is_integer(response["expires_in"])

    assert response["interval"] ==
             astrenv(:oauth2_flow_device_authorization_rate_limiter_interval)

    {:ok, device_code} = DeviceCode.get(response["device_code"])

    assert device_code.user_code == response["user_code"]
    assert device_code.data["clid"] == "client_confidential_1"
    assert device_code.data["sjid"] == nil
    assert device_code.data["requested_scopes"] == []
    assert device_code.data["granted_scopes"] == nil
    assert device_code.data["status"] == "authorization_pending"
  end

  test "successful request confidential client with scopes", %{conn: conn} do
    req = %{"scope" => "scp1 scp2 scp5 scp6"}

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(200)

    assert is_binary(response["device_code"])
    assert is_binary(response["user_code"])
    assert is_binary(response["verification_uri"])
    assert is_binary(response["verification_uri_complete"])
    assert is_integer(response["expires_in"])

    assert response["interval"] ==
             astrenv(:oauth2_flow_device_authorization_rate_limiter_interval)

    # there is not such an attribute returned
    assert response["scope"] == nil

    {:ok, device_code} = DeviceCode.get(response["device_code"])

    assert device_code.user_code == response["user_code"]
    assert device_code.data["clid"] == "client_confidential_1"
    assert device_code.data["sjid"] == nil
    assert device_code.data["granted_scopes"] == nil

    assert Scope.Set.equal?(
             Scope.Set.new(device_code.data["requested_scopes"]),
             Scope.Set.new(["scp1", "scp2", "scp6", "scp5"])
           )

    assert device_code.data["status"] == "authorization_pending"
  end

  test "successful request public client with scopes", %{conn: conn} do
    req = %{
      "scope" => "scp1 scp3 scp5",
      "client_id" => "client_public_1"
    }

    response =
      conn
      |> post(Routes.device_authorization_path(conn, :handle), req)
      |> json_response(200)

    assert is_binary(response["device_code"])
    assert is_binary(response["user_code"])
    assert is_binary(response["verification_uri"])
    assert is_binary(response["verification_uri_complete"])
    assert is_integer(response["expires_in"])

    assert response["interval"] ==
             astrenv(:oauth2_flow_device_authorization_rate_limiter_interval)

    # there is not such an attribute returned
    assert response["scope"] == nil

    {:ok, device_code} = DeviceCode.get(response["device_code"])

    assert device_code.user_code == response["user_code"]
    assert device_code.data["clid"] == "client_public_1"
    assert device_code.data["sjid"] == nil
    assert device_code.data["granted_scopes"] == nil

    assert Scope.Set.equal?(
             Scope.Set.new(device_code.data["requested_scopes"]),
             Scope.Set.new(["scp1", "scp3", "scp5"])
           )

    assert device_code.data["status"] == "authorization_pending"
  end

  defp basic_auth_header(client, secret) do
    "Basic " <> Base.encode64(client <> ":" <> secret)
  end
end


<|start_filename|>lib/asteroid/object_store/access_token.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AccessToken do
  @moduledoc """
  Behaviour for access token store
  """

  @type opts :: Keyword.t()

  @doc """
  Installs the access token store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the access token store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the access token store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an access token from its id

  Returns `{:ok, %Asteroid.Token.AccessToken{}}` if the access token exists and `{:ok, nil}`
  otherwise.
  """

  @callback get(Asteroid.Token.AccessToken.id(), opts()) ::
              {:ok, Asteroid.Token.AccessToken.t() | nil}
              | {:error, any()}

  @doc """
  Returns all the *access token ids* from a refresh token id
  """

  @callback get_from_refresh_token_id(Asteroid.Token.RefreshToken.id(), opts()) ::
              {:ok, [Asteroid.Token.AccessToken.id()]} | {:error, any()}

  @doc """
  Returns all the *access token ids* of a subject
  """

  @callback get_from_subject_id(Asteroid.Subject.id(), opts()) ::
              {:ok, [Asteroid.Token.AccessToken.id()]} | {:error, any()}

  @doc """
  Returns all the *access token ids* of a client
  """

  @callback get_from_client_id(Asteroid.Client.id(), opts()) ::
              {:ok, [Asteroid.Token.AccessToken.id()]} | {:error, any()}

  @doc """
  Returns all the *access token ids* of a device
  """

  @callback get_from_device_id(Asteroid.Device.id(), opts()) ::
              {:ok, [Asteroid.Token.AccessToken.id()]} | {:error, any()}

  @doc """
  Stores an access token

  If the access token already exists, all of its data should be erased.
  """

  @callback put(Asteroid.Token.AccessToken.t(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an access token
  """

  @callback delete(Asteroid.Token.AccessToken.id(), opts()) :: :ok | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1,
                      get_from_subject_id: 2,
                      get_from_client_id: 2,
                      get_from_device_id: 2
end


<|start_filename|>lib/asteroid/oauth2/apiac_auth_bearer/validator.ex<|end_filename|>
defmodule Asteroid.OAuth2.APIacAuthBearer.Validator do
  @moduledoc """
  Implementation of `APIacAuthBearer.Validator` for Asteroid

  This implementation checks the access token against Asteroid's own access token store and can
  be used on Asteroid endpoints on which clients can authenticate using the HTTP `Bearer`
  scheme, such as `/introspect` and `/register`.

  There are no options.
  """

  alias Asteroid.Token.AccessToken

  @behaviour APIacAuthBearer.Validator

  @impl true

  def validate(access_token_param, _) do
    case AccessToken.get(access_token_param) do
      {:ok, access_token} ->
        {:ok, access_token.data}

      {:error, _} = error ->
        error
    end
  end
end


<|start_filename|>lib/asteroid/object_store/device_code/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.DeviceCode.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.DeviceCode` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.DeviceCode.opts()`) are:
  - `:table_name`: an `atom()` for the table name. Defaults to `:asteroid_device_code`
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `300` (5 minutes)

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :refresh_token_id, :subject_id, :client_id, :device_id, :data],
    index: [:refresh_token_id]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  alias Asteroid.Token.DeviceCode

  @behaviour Asteroid.ObjectStore.DeviceCode

  @impl true

  def install(opts) do
    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    table_name = opts[:table_name] || :asteroid_device_code

    tab_def =
      [
        attributes: [:id, :user_code, :data],
        index: [:user_code]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(table_name, tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created device code store #{table_name}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: device code store #{table_name} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create device code store #{table_name} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 300], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(device_code_id, opts) do
    table_name = opts[:table_name] || :asteroid_device_code

    case :mnesia.dirty_read(table_name, device_code_id) do
      [] ->
        Logger.debug("#{__MODULE__}: getting device code `#{device_code_id}`, " <> "value: `nil`")

        {:ok, nil}

      [
        {^table_name, ^device_code_id, user_code, data}
      ] ->
        device_code =
          DeviceCode.new(
            id: device_code_id,
            user_code: user_code,
            data: data
          )

        Logger.debug(
          "#{__MODULE__}: getting device code `#{device_code_id}`, " <>
            "value: `#{inspect(device_code)}`"
        )

        {:ok, device_code}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_user_code(user_code, opts) do
    table_name = opts[:table_name] || :asteroid_device_code

    [{_table_name, device_code_id, _user_code, _data}] =
      :mnesia.dirty_match_object({table_name, :_, user_code, :_})

    get(device_code_id, opts)
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(device_code, opts) do
    table_name = opts[:table_name] || :asteroid_device_code

    record = {
      table_name,
      device_code.id,
      device_code.user_code,
      device_code.data
    }

    :mnesia.dirty_write(table_name, record)

    Logger.debug(
      "#{__MODULE__}: stored device code `#{device_code.id}`, " <>
        "value: `#{inspect(device_code)}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(device_code_id, opts) do
    table_name = opts[:table_name] || :asteroid_device_code

    :mnesia.dirty_delete(table_name, device_code_id)

    Logger.debug("#{__MODULE__}: deleted device code `#{device_code_id}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>lib/asteroid/token/authorization_code.ex<|end_filename|>
defmodule Asteroid.Token.AuthorizationCode do
  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.Client
  alias Asteroid.Token

  @moduledoc """
  Authorization code structure

  ## Field naming
  The `data` field holds the token data. The following field names are standard and are used
  by Asteroid:
  - `"exp"`: the expiration unix timestamp of the authorization code
  - `"sub"`: the `t:Asteroid.Subject.id/0` of the authorization code
  - `"client_id"`: the `t:Asteroid.Client.id/0` of the authorization code
  - `"device_id"`: the `t:Asteroid.Device.id/0` of the authorization code
  - `"requested_scopes"`: a list of `OAuth2Utils.Scope.scope()` requested scopes
  - `"granted_scopes"`: a list of `OAuth2Utils.Scope.scope()` granted scopes
  - `"__asteroid_oauth2_initial_flow"`: the initial `t:Asteroid.OAuth2.flow_str/0` during which
  the authorization code was granted
  - `"__asteroid_oauth2_pkce_code_challenge"`: the PKCE code challenge, if any
  - `"__asteroid_oauth2_pkce_code_challenge_method"`: the PKCE code challenge method, if any,
  stored as a `t:Asteroid.OAuth2.PKCE.code_challenge_method_str/0`
  - `"__asteroid_oidc_nonce"`: the OIDC nonce, if any
  - `"__asteroid_oidc_claims"`: the claims that were requested, if any
  - `"__asteroid_oidc_authenticated_session_id"`: the `t:Asteroid.OIDC.AuthenticatedSession.id/0`
  of the authroization code, if any
  - `"__asteroid_oidc_initial_acr"`: the `t:Asteroid.OIDC.acr/0` of the authorization code, if
  any. This is the value got from the session when the token was first released
  - `"__asteroid_oidc_initial_amr"`: a list of `t:Asteroid.OIDC.acr/0` of the authorization code,
  if any. This is the value got from the session when the token was first released
  - `"__asteroid_oidc_initial_auth_time"`: a `non_neg_integer()` of the authorization code,
  if any. This is the value got from the session when the token was first released
  """

  @enforce_keys [:id, :serialization_format, :data]

  defstruct [:id, :data, :serialization_format]

  @type id :: binary()

  @type t :: %__MODULE__{
          id: __MODULE__.id(),
          serialization_format: Asteroid.Token.serialization_format(),
          data: map()
        }

  @doc ~s"""
  Creates a new authorization code struct

  ## Options
  - `:id`: `String.t()` id, **mandatory**
  - `:data`: a data `map()`
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  """

  @spec new(Keyword.t()) :: t()

  def new(opts) do
    %__MODULE__{
      id: opts[:id] || raise("Missing authorization code id"),
      data: opts[:data] || %{},
      serialization_format: opts[:serialization_format] || :opaque
    }
  end

  @doc """
  Generates a new authorization code

  ## Options
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  """

  @spec gen_new(Keyword.t()) :: t()

  def gen_new(opts \\ []) do
    %__MODULE__{
      id: secure_random_b64(),
      data: %{},
      serialization_format: opts[:format] || :opaque
    }
  end

  @doc """
  Gets a authorization code from the authorization code store

  Unlike the `c:Asteroid.ObjectStore.AuthorizationCode.get/2`, this function returns
  `{:error, %Asteroid.Token.InvalidTokenError{}}` if the authorization code is not found in
  the token store.

  ## Options
  - `:check_active`: determines whether the validity of the authorization code should be checked.
  Defaults to `true`. For validity checking details, see `active?/1`
  """

  @spec get(id(), Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}

  def get(authorization_code_id, opts \\ [check_active: true]) do
    azcode_store_module = astrenv(:object_store_authorization_code)[:module]
    azcode_store_opts = astrenv(:object_store_authorization_code)[:opts] || []

    case azcode_store_module.get(authorization_code_id, azcode_store_opts) do
      {:ok, authorization_code} when not is_nil(authorization_code) ->
        if opts[:check_active] != true or active?(authorization_code) do
          {:ok, authorization_code}
        else
          {:error,
           Token.InvalidTokenError.exception(
             sort: "authorization code",
             reason: "inactive token",
             id: authorization_code_id
           )}
        end

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "authorization code",
           reason: "not found in the token store",
           id: authorization_code_id
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Stores a authorization code
  """

  @spec store(t(), Context.t()) :: {:ok, t()} | {:error, any()}

  def store(authorization_code, ctx \\ %{}) do
    azcode_store_module = astrenv(:object_store_authorization_code)[:module]
    azcode_store_opts = astrenv(:object_store_authorization_code)[:opts] || []

    authorization_code =
      astrenv(:object_store_authorization_code_before_store_callback).(authorization_code, ctx)

    case azcode_store_module.put(authorization_code, azcode_store_opts) do
      :ok ->
        {:ok, authorization_code}

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Deletes a authorization code from its store
  """

  @spec delete(t() | id()) :: :ok | {:error, any()}

  def delete(%__MODULE__{id: id}) do
    delete(id)
  end

  def delete(authorization_code_id) do
    azcode_store_module = astrenv(:object_store_authorization_code)[:module]
    azcode_store_opts = astrenv(:object_store_authorization_code)[:opts] || []

    azcode_store_module.delete(authorization_code_id, azcode_store_opts)
  end

  @doc """
  Puts a value into the `data` field of an authorization code

  If the value is `nil`, the authorization code is not changed and the field is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(authorization_code, _key, nil), do: authorization_code

  def put_value(authorization_code, key, val) do
    %{authorization_code | data: Map.put(authorization_code.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of a authorization code

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(authorization_code, key) do
    %{authorization_code | data: Map.delete(authorization_code.data, key)}
  end

  @doc """
  Serializes the authorization code, using its inner `t:Asteroid.Token.serialization_format/0`
  information

  Supports serialization to `:opaque` serialization format.
  """

  @spec serialize(t()) :: String.t()

  def serialize(%__MODULE__{id: id, serialization_format: :opaque}) do
    id
  end

  @doc """
  Returns `true` if the token is active, `false` otherwise

  The following data, *when set*, are used to determine that a token is active:
  - `"nbf"`: must be lower than current time
  - `"exp"`: must be higher than current time
  """

  @spec active?(t()) :: boolean()

  def active?(authorization_code) do
    (is_nil(authorization_code.data["nbf"]) or authorization_code.data["nbf"] < now()) and
      (is_nil(authorization_code.data["exp"]) or authorization_code.data["exp"] > now())
  end

  @doc """
  Returns the authorization code lifetime

  ## Processing rules
  - If the client has the following field set to an integer value for the corresponding flow
  returns that value:
    - `"__asteroid_oauth2_flow_authorization_code_authorization_code_lifetime"`
    - `"__asteroid_oidc_flow_authorization_code_authorization_code_lifetime"`
    - `"__asteroid_oidc_flow_hybrid_authorization_code_lifetime"`
  - Otherwise, if the following configuration option is set to an integer for the corresponding
  flow, returns its value:
    - #{
    Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_authorization_code_lifetime)
  }
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_authorization_code_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_authorization_code_lifetime)}
  - otherwise uses the value of the
  #{Asteroid.Config.link_to_option(:oauth2_authorization_code_lifetime)} configuration option
  - Otherwise returns `0`
  """

  def lifetime(%{flow: flow, endpoint: :authorize, client: client}) do
    {attr, conf_opt} =
      case flow do
        :authorization_code ->
          {"__asteroid_oauth2_flow_authorization_code_authorization_code_lifetime",
           :oauth2_flow_authorization_code_authorization_code_lifetime}

        :oidc_authorization_code ->
          {"__asteroid_oidc_flow_authorization_code_authorization_code_lifetime",
           :oidc_flow_authorization_code_authorization_code_lifetime}

        :oidc_hybrid ->
          {"__asteroid_oidc_flow_hybrid_authorization_code_lifetime",
           :oidc_flow_hybrid_authorization_code_lifetime}
      end

    client = Client.fetch_attributes(client, [attr])

    case client.attrs[attr] do
      lifetime when is_integer(lifetime) ->
        lifetime

      _ ->
        astrenv(conf_opt, astrenv(:oauth2_authorization_code_lifetime, 0))
    end
  end

  def lifetime(_) do
    0
  end
end


<|start_filename|>lib/asteroid/object_store/refresh_token.ex<|end_filename|>
defmodule Asteroid.ObjectStore.RefreshToken do
  @moduledoc """
  Behaviour for refresh token store
  """

  @type opts :: Keyword.t()

  @doc """
  Installs the refresh token store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the refresh token store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the refresh token store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an refresh token from its id

  Returns `{:ok, %Asteroid.Token.RefreshToken{}}` if the refresh token exists and `{:ok, nil}`
  otherwise.
  """

  @callback get(Asteroid.Token.RefreshToken.id(), opts()) ::
              {:ok, Asteroid.Token.RefreshToken.t() | nil}
              | {:error, any()}

  @doc """
  Returns all the *refresh token ids* of a subject
  """

  @callback get_from_subject_id(Asteroid.Subject.id(), opts()) ::
              {:ok, [Asteroid.Token.RefreshToken.id()]} | {:error, any()}

  @doc """
  Returns all the *refresh token ids* of a client
  """

  @callback get_from_client_id(Asteroid.Client.id(), opts()) ::
              {:ok, [Asteroid.Token.RefreshToken.id()]} | {:error, any()}

  @doc """
  Returns all the *refresh token ids* of a device
  """

  @callback get_from_device_id(Asteroid.Device.id(), opts()) ::
              {:ok, [Asteroid.Token.RefreshToken.id()]} | {:error, any()}

  @doc """
  Returns all the *refresh token ids* issued during an authenticated session

  This callback is made optionnal in case authenticated sessions are not used. If it is,
  this callback will be used and its implementation is therefore mandatory.
  """

  @callback get_from_authenticated_session_id(Asteroid.OIDC.AuthenticatedSession.id(), opts()) ::
              {:ok, [Asteroid.Token.RefreshToken.id()]} | {:error, any()}

  @doc """
  Stores an refresh token

  If the refresh token already exists, all of its data should be erased.
  """

  @callback put(Asteroid.Token.RefreshToken.t(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an refresh token
  """

  @callback delete(Asteroid.Token.RefreshToken.id(), opts()) :: :ok | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1,
                      get_from_subject_id: 2,
                      get_from_client_id: 2,
                      get_from_device_id: 2,
                      get_from_authenticated_session_id: 2
end


<|start_filename|>lib/asteroid_web/controllers/api/oidc/userinfo_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.OIDC.UserinfoController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias Asteroid.OIDC
  alias Asteroid.Subject
  alias Asteroid.Token.AccessToken

  @client_userinfo_crypto_fields [
    "userinfo_signed_response_alg",
    "userinfo_encrypted_response_alg",
    "userinfo_encrypted_response_enc"
  ]

  def show(conn, _params) do
    with {:ok, access_token} <- AccessToken.get(APIac.metadata(conn)["bearer"]),
         claims <- claims_for_access_token(access_token),
         {:ok, client} <-
           Client.load_from_unique_attribute("client_id", access_token.data["client_id"]),
         {:ok, subject} <-
           Subject.load_from_unique_attribute("sub", access_token.data["sub"], attributes: claims) do
      client = Client.fetch_attributes(client, @client_userinfo_crypto_fields)

      ctx =
        %{}
        |> Map.put(:endpoint, :userinfo)
        |> Map.put(:client, client)
        |> Map.put(:subject, subject)
        |> Map.put(:access_token, access_token)
        |> Map.put(:conn, conn)

      result_claims =
        Enum.reduce(
          claims,
          %{},
          fn
            claim, acc ->
              put_if_not_nil(acc, claim, subject.attrs[claim])
          end
        )
        |> Map.put("sub", astrenv(:oidc_subject_identifier_callback).(subject, client))
        |> put_iss_aud_if_signed(client)
        |> astrenv(:oidc_endpoint_userinfo_before_send_resp_callback).(ctx)
        |> maybe_sign(client)
        |> maybe_encrypt(client)

      case result_claims do
        %{} ->
          conn
          |> put_status(200)
          |> put_resp_content_type("application/json")
          |> astrenv(:oidc_endpoint_userinfo_before_send_conn_callback).(ctx)
          |> json(result_claims)

        _ ->
          conn
          |> put_status(200)
          |> put_resp_content_type("application/jwt")
          |> astrenv(:oidc_endpoint_userinfo_before_send_conn_callback).(ctx)
          |> text(result_claims)
      end
    else
      {:error, err} ->
        AsteroidWeb.Error.respond_api(conn, OAuth2.ServerError.exception(reason: inspect(err)))
    end
  rescue
    e ->
      conn
      |> put_status(500)
      |> text(Exception.message(e))
  end

  @spec claims_for_access_token(AccessToken.t()) :: [String.t()]

  defp claims_for_access_token(access_token) do
    claims_to_exclude = [
      "iss",
      "sub",
      "aud",
      "exp",
      "iat",
      "auth_time",
      "nonce",
      "acr",
      "amr",
      "azp"
    ]

    claims_from_param =
      (access_token.data["__asteroid_oidc_claims"]["userinfo"] || %{})
      |> Map.keys()
      |> Enum.filter(&(&1 not in claims_to_exclude))

    Enum.reduce(
      access_token.data["scope"] || [],
      claims_from_param,
      fn
        scope, acc when scope in unquote(Map.keys(OIDC.Userinfo.scope_claims_mapping())) ->
          OIDC.Userinfo.scope_claims_mapping()[scope] ++ acc

        _, acc ->
          acc
      end
    )
  end

  @spec put_iss_aud_if_signed(map(), Client.t()) :: map()

  defp put_iss_aud_if_signed(claims, client) do
    if client.attrs["userinfo_signed_response_alg"] do
      claims
      |> Map.put("iss", OAuth2.issuer())
      |> Map.put("aud", client.attrs["client_id"])
    else
      claims
    end
  end

  @spec maybe_sign(map(), Client.t()) :: String.t() | map()

  defp maybe_sign(claims, client) do
    signing_alg = client.attrs["userinfo_signed_response_alg"]

    if signing_alg do
      eligible_jwks =
        Crypto.Key.get_all()
        |> Enum.filter(fn
          %JOSE.JWK{fields: fields} ->
            fields["use"] == "sig" and
              (fields["key_ops"] in ["sign"] or fields["key_ops"] == nil) and
              (fields["alg"] == signing_alg or fields["alg"] == nil)
        end)

      case eligible_jwks do
        [jwk | _] ->
          JOSE.JWT.sign(jwk, JOSE.JWS.from_map(%{"alg" => signing_alg}), claims)
          |> JOSE.JWS.compact()
          |> elem(1)

        [] ->
          raise Crypto.Key.NoSuitableKeyError
      end
    else
      claims
    end
  end

  @spec maybe_encrypt(map() | String.t(), Client.t()) :: String.t() | map()

  defp maybe_encrypt(claims_or_jws, client) do
    encryption_alg = client.attrs["userinfo_encrypted_response_alg"]

    if encryption_alg do
      case Client.get_jwks(client) do
        {:ok, keys} ->
          eligible_jwks =
            keys
            |> Enum.map(&JOSE.JWK.from/1)
            |> Enum.filter(fn
              %JOSE.JWK{fields: fields} ->
                (fields["use"] == "enc" or fields["use"] == nil) and
                  (fields["key_ops"] == "encrypt" or fields["key_ops"] == nil) and
                  (fields["alg"] == encryption_alg or fields["alg"] == nil)
            end)

          case eligible_jwks do
            [jwk | _] ->
              payload =
                case claims_or_jws do
                  %{} ->
                    Jason.encode!(claims_or_jws)

                  _ ->
                    claims_or_jws
                end

              encryption_enc = client.attrs["userinfo_encrypted_response_enc"] || "A128CBC-HS256"

              JOSE.JWE.block_encrypt(
                jwk,
                payload,
                %{"alg" => encryption_alg, "enc" => encryption_enc}
              )
              |> JOSE.JWE.compact()
              |> elem(1)

            [] ->
              raise Crypto.Key.NoSuitableKeyError
          end

        {:error, _} ->
          raise Crypto.Key.NoSuitableKeyError
      end
    else
      claims_or_jws
    end
  end
end


<|start_filename|>lib/asteroid/object_store/authorization_code/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthorizationCode.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.AuthorizationCode` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.AuthorizationCode.opts()`) are:
  - `:table_name`: an `atom()` for the table name. Defaults to `:asteroid_authorization_code`
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `240` (3 minutes)

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :data]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  alias Asteroid.Token.AuthorizationCode

  @behaviour Asteroid.ObjectStore.AuthorizationCode

  @impl true

  def install(opts) do
    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    table_name = opts[:table_name] || :asteroid_authorization_code

    tab_def =
      [
        attributes: [:id, :data]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(table_name, tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created authorization code store #{table_name}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: authorization code store #{table_name} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create authorization code store #{table_name} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 240], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(authorization_code_id, opts) do
    table_name = opts[:table_name] || :asteroid_authorization_code

    case :mnesia.dirty_read(table_name, authorization_code_id) do
      [] ->
        Logger.debug(
          "#{__MODULE__}: getting authorization code `#{authorization_code_id}`, " <>
            "value: `nil`"
        )

        {:ok, nil}

      [{^table_name, ^authorization_code_id, data}] ->
        authorization_code =
          AuthorizationCode.new(
            id: authorization_code_id,
            data: data
          )

        Logger.debug(
          "#{__MODULE__}: getting authorization code `#{authorization_code_id}`, " <>
            "value: `#{inspect(authorization_code)}`"
        )

        {:ok, authorization_code}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(authorization_code, opts) do
    table_name = opts[:table_name] || :asteroid_authorization_code

    record = {
      table_name,
      authorization_code.id,
      authorization_code.data
    }

    :mnesia.dirty_write(table_name, record)

    Logger.debug(
      "#{__MODULE__}: stored authorization code `#{authorization_code.id}`, " <>
        "value: `#{inspect(authorization_code)}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(authorization_code_id, opts) do
    table_name = opts[:table_name] || :asteroid_authorization_code

    :mnesia.dirty_delete(table_name, authorization_code_id)

    Logger.debug("#{__MODULE__}: deleted authorization code `#{authorization_code_id}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>lib/asteroid/application.ex<|end_filename|>
defmodule Asteroid.Application do
  @moduledoc false

  use Application

  import Asteroid.Utils

  alias Asteroid.AttributeRepository
  alias Asteroid.ObjectStore
  alias Asteroid.Crypto

  def start(_type, _args) do
    children = [
      AsteroidWeb.Endpoint
    ]

    with :ok <- AttributeRepository.auto_install_from_config(),
         :ok <- AttributeRepository.auto_start_from_config(),
         :ok <- ObjectStore.auto_install_from_config(),
         :ok <- ObjectStore.auto_start_from_config(),
         :ok <- Crypto.Key.load_from_config!() do
      if astrenv(:crypto_jws_none_alg_enabled, false) do
        JOSE.JWA.unsecured_signing(true)
      end

      opts = [strategy: :one_for_one, name: Asteroid.Supervisor]
      Supervisor.start_link(children, opts)
    end
  end

  def config_change(changed, _new, removed) do
    AsteroidWeb.Endpoint.config_change(changed, removed)
    :ok
  end
end


<|start_filename|>lib/asteroid/oidc/userinfo.ex<|end_filename|>
defmodule Asteroid.OIDC.Userinfo do
  @moduledoc """
  Convenience functions to work with the `/userinfo` endpoint
  """

  @doc """
  Returns the mapping between the `"email"`, `"profile"`, `"phone"` and `"address"` scope
  and their corresponding claims

  ```elixir
  %{
    "profile" => [
      "name",
      "family_name",
      "given_name",
      "middle_name",
      "nickname",
      "preferred_username",
      "profile",
      "picture",
      "website",
      "gender",
      "birthdate",
      "zoneinfo",
      "locale",
      "updated_at"
    ],
    "email" => ["email", "email_verified"],
    "address" => ["address"],
    "phone" => ["phone_number","phone_number_verified"]
  }
  ```
  """

  @spec scope_claims_mapping() :: %{required(String.t()) => [String.t()]}

  def scope_claims_mapping() do
    %{
      "profile" => [
        "name",
        "family_name",
        "given_name",
        "middle_name",
        "nickname",
        "preferred_username",
        "profile",
        "picture",
        "website",
        "gender",
        "birthdate",
        "zoneinfo",
        "locale",
        "updated_at"
      ],
      "email" => ["email", "email_verified"],
      "address" => ["address"],
      "phone" => ["phone_number", "phone_number_verified"]
    }
  end
end


<|start_filename|>test/support/OIDC.ex<|end_filename|>
defmodule TestOIDCHelpers do
  def token_hash(signing_alg, token) do
    hash_alg =
      cond do
        signing_alg in [:ES256, :HS256, :PS256, :RS256] ->
          :sha256

        signing_alg in [:ES384, :HS384, :PS384, :RS384] ->
          :sha384

        signing_alg in [:ES512, :HS512, :PS512, :RS512] ->
          :sha512
      end

    digest = :crypto.hash(hash_alg, token)

    digest
    |> :binary.part({0, div(byte_size(digest), 2)})
    |> Base.url_encode64(padding: false)
  end
end


<|start_filename|>lib/asteroid/token/device_code.ex<|end_filename|>
defmodule Asteroid.Token.DeviceCode do
  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.Client
  alias Asteroid.OAuth2
  alias Asteroid.Token

  @moduledoc """
  Device code structure

  Note that this token is searched using 2 keys:
  - the device code (when polling)
  - the user code, to mark this token as `"granted"` or `"denied"` upon completion of
  the user web flow
  Thus, even though the primary key is the device code id, the user code should probably be
  indexed as well.

  ## Field naming
  The `data` field holds the token data. The following field names are standard and are used
  by Asteroid:
  - `"exp"`: the expiration unix timestamp of the device code
  - `"clid"`: the `t:Asteroid.Client.id()` of the device code
  - `"sjid"`: the `t:Asteroid.Subject.id()` of the user that has accepted the request,
  after entering the user code in the web flow
  - `"requested_scopes"`: a list of `OAuth2Utils.Scope.scope()` requested scopes
  - `"granted_scopes"`: a list of `OAuth2Utils.Scope.scope()` granted scopes
  - `"status"`: a `String.t()` for the status of the device code. Mandatory, one of:
    - `"authorization_pending"`: the user has not yet granted or denied the request (the default
    value upon the token's creation)
    - `"granted"`: the user has granted the request
    - `"denied"`: the user has denied the request
  """

  @enforce_keys [:id, :user_code, :serialization_format, :data]

  defstruct [:id, :user_code, :serialization_format, :data]

  @type t :: %__MODULE__{
          id: OAuth2.DeviceAuthorization.device_code(),
          user_code: binary() | nil,
          serialization_format: Asteroid.Token.serialization_format(),
          data: map()
        }

  @doc ~s"""
  Creates a new device code

  ## Options
  - `:id`: `String.t()` id, **mandatory**
  - `:user_code`: the `t:Asteroid.OAuth2.DeviceAuthorization.user_code/0` associated to the
  device code. **Mandatory**
  - `:data`: a data `map()`
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  """

  @spec new(Keyword.t()) :: t()

  def new(opts) do
    %__MODULE__{
      id: opts[:id] || raise("Missing device code"),
      user_code: opts[:user_code] || raise("Missing user code"),
      data: opts[:data] || %{},
      serialization_format: opts[:serialization_format] || :opaque
    }
  end

  @doc """
  Generates a new device code

  ## Options
  - `:user_code`: the user code to be presented to the user. **Mandatory**
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  """

  @spec gen_new(Keyword.t()) :: t()
  def gen_new(opts \\ []) do
    %__MODULE__{
      id: secure_random_b64(),
      user_code: opts[:user_code] || raise("Missing user code"),
      data: %{},
      serialization_format:
        if(opts[:serialization_format], do: opts[:serialization_format], else: :opaque)
    }
  end

  @doc """
  Gets a device code from the device code store

  ## Options
  - `:check_active`: determines whether the validity of the device code should be checked.
  Defaults to `true`. For validity checking details, see `active?/1`
  """

  @spec get(OAuth2.DeviceAuthorization.device_code(), Keyword.t()) ::
          {:ok, t()}
          | {:error, Exception.t()}

  def get(device_code_id, opts \\ [check_active: true]) do
    code_store_module = astrenv(:object_store_device_code)[:module]
    code_store_opts = astrenv(:object_store_device_code)[:opts] || []

    case code_store_module.get(device_code_id, code_store_opts) do
      {:ok, device_code} when not is_nil(device_code) ->
        if opts[:check_active] != true or active?(device_code) do
          {:ok, device_code}
        else
          {:error,
           Token.InvalidTokenError.exception(
             sort: "device code",
             reason: "expired code",
             id: device_code_id
           )}
        end

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "device code",
           reason: "not found in the token store",
           id: device_code_id
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Gets a device code from the device code store from its associated user code

  ## Options
  - `:check_active`: determines whether the validity of the device code should be checked.
  Defaults to `true`. For validity checking details, see `active?/1`
  """

  @spec get_from_user_code(OAuth2.DeviceAuthorization.user_code(), Keyword.t()) ::
          {:ok, t()}
          | {:error, Exception.t()}

  def get_from_user_code(user_code, opts \\ [check_active: true]) do
    code_store_module = astrenv(:object_store_device_code)[:module]
    code_store_opts = astrenv(:object_store_device_code)[:opts] || []

    case code_store_module.get_from_user_code(user_code, code_store_opts) do
      {:ok, device_code} when not is_nil(device_code) ->
        if opts[:check_active] != true or active?(device_code) do
          {:ok, device_code}
        else
          {:error,
           Token.InvalidTokenError.exception(
             sort: "device code",
             reason: "inactive token",
             id: device_code.id
           )}
        end

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "device code",
           reason: "invalid user code",
           id: user_code
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Stores a device code
  """

  @spec store(t(), Context.t()) :: {:ok, t()} | {:error, any()}

  def store(device_code, ctx) do
    code_store_module = astrenv(:object_store_device_code)[:module]
    code_store_opts = astrenv(:object_store_device_code)[:opts] || []

    device_code = astrenv(:object_store_device_code_before_store_callback).(device_code, ctx)

    case code_store_module.put(device_code, code_store_opts) do
      :ok ->
        {:ok, device_code}

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Deletes a device code
  """

  @spec delete(t() | OAuth2.DeviceAuthorization.device_code()) :: :ok | {:error, any()}

  def delete(%__MODULE__{id: id}) do
    delete(id)
  end

  def delete(device_code_id) do
    code_store_module = astrenv(:object_store_device_code)[:module]
    code_store_opts = astrenv(:object_store_device_code)[:opts] || []

    code_store_module.delete(device_code_id, code_store_opts)
  end

  @doc """
  Puts a value into the `data` field of a device code

  If the value is `nil`, the device code is not changed and the filed is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(device_code, _key, nil), do: device_code

  def put_value(device_code, key, val) do
    %{device_code | data: Map.put(device_code.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of a device code

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(device_code, key) do
    %{device_code | data: Map.delete(device_code.data, key)}
  end

  @doc """
  Serializes the device code, using its inner `t:Asteroid.Token.serialization_format/0`
  information

  Supports serialization to `:opaque`.
  """

  @spec serialize(t()) :: String.t()

  def serialize(%__MODULE__{id: id, serialization_format: :opaque}) do
    id
  end

  @doc """
  Returns `true` if the token is active, `false` otherwise

  The following data, *when set*, are used to determine that a token is active:
  - `"exp"`: must be higher than current time
  """

  @spec active?(t()) :: boolean()
  def active?(device_code) do
    is_nil(device_code.data["exp"]) or device_code.data["exp"] > now()
  end

  @doc """
  Returns the device code lifetime

  ## Processing rules
  - If the client has the
  `"__asteroid_oauth2_flow_device_authorization_device_code_lifetime"` set to an integer,
  returns this value
  - Otherwise, if the
  #{Asteroid.Config.link_to_option(:oauth2_flow_device_authorization_device_code_lifetime)}
  #configuration option is set, return this value
  - Otherwise returns `0`
  """

  @spec lifetime(Context.t()) :: non_neg_integer()

  def lifetime(%{client: client}) do
    attr = "__asteroid_oauth2_flow_device_authorization_device_code_lifetime"

    client = Client.fetch_attributes(client, [attr])

    case client.attrs[attr] do
      lifetime when is_integer(lifetime) ->
        lifetime

      _ ->
        astrenv(:oauth2_flow_device_authorization_device_code_lifetime, 0)
    end
  end
end


<|start_filename|>lib/asteroid/object_store/generic_kv/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.GenericKV.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.GenericKV` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.GenericKV.opts()`) are:
  - `:table_name`: an `atom()` for the table name. No default, **mandatory**
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `720` (12 minutes). The purge uses the `"exp"`
  attribute of the value map as the expiration unix timestamp. If the value is not a map, it
  cannot be purged.

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :data]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  @behaviour Asteroid.ObjectStore.GenericKV

  @impl true

  def install(opts) do
    unless is_atom(opts[:table_name]) do
      raise "Table name not specified for #{__MODULE__} store"
    end

    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    tab_def =
      [
        attributes: [:id, :data]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(opts[:table_name], tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created generiv KV store #{opts[:table_name]}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: generic KV store #{opts[:table_name]} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create generic KV store #{opts[:table_name]} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 12 * 60], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(key, opts) do
    table_name = opts[:table_name] || raise "Table name not specified for #{__MODULE__} store"

    case :mnesia.dirty_read(table_name, key) do
      [] ->
        {:ok, nil}

      [{^table_name, ^key, data}] ->
        {:ok, data}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(key, value, opts) do
    table_name = opts[:table_name] || raise "Table name not specified for #{__MODULE__} store"

    :mnesia.dirty_write({table_name, key, value})

    Logger.debug(
      "#{__MODULE__}: stored object `#{inspect(key)}`, " <>
        "value: `#{inspect(value)}` in table `#{table_name}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(key, opts) do
    table_name = opts[:table_name] || raise "Table name not specified for #{__MODULE__} store"

    :mnesia.dirty_delete(table_name, key)

    Logger.debug("#{__MODULE__}: deleted object `#{key}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>lib/asteroid/object_store/refresh_token/riak.ex<|end_filename|>
defmodule Asteroid.ObjectStore.RefreshToken.Riak do
  @moduledoc """
  Riak implementation of the `Asteroid.ObjectStore.RefreshToken` behaviour

  ## Initializing a Riak bucket type

  ```console
  $ sudo riak-admin bucket-type create token '{"props":{"datatype":"map", "backend":"bitcask_mult"}}'
  token created

  $ sudo riak-admin bucket-type activate token
  token has been activated
  ```

  ## Options
  The options (`Asteroid.ObjectStore.RefreshToken.opts()`) are:
  - `:bucket_type`: an `String.t()` for the bucket type that must be created beforehand in
  Riak. No defaults, **mandatory**
  - `bucket_name`: a `String.t()` for the bucket name. Defaults to `"refresh_token"`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `1200` (20 minutes)
  - `:rows`: the maximum number of results that a search will return. Defaults to `1_000_000`.
  Search is used by the purge process.

  ## Installation function

  The `install/1` function executes the following actions:
  - it installs a custom schema (`asteroid_object_store_refresh_token_riak_schema`)
  - it creates a new index (`asteroid_object_store_refresh_token_riak_index`) on the bucket
  (and not the bucket type - so as to avoid collisions)

  This is necessary to:
  1. Efficiently index expiration timestamp
  2. Disable indexing of raw refresh token data

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Riak).

  """

  require Logger

  @behaviour Asteroid.ObjectStore.RefreshToken

  @impl true

  def install(opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "refresh_token"

    with :ok <-
           Riak.Search.Schema.create(
             schema_name(),
             (:code.priv_dir(:asteroid) ++ '/riak/object_store_refresh_token_schema.xml')
             |> File.read!()
           ),
         :ok <- Riak.Search.Index.put(index_name(), schema_name()),
         :ok <- Riak.Search.Index.set({bucket_type, bucket_name}, index_name()) do
      Logger.info(
        "#{__MODULE__}: created refresh token store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}`"
      )

      :ok
    else
      e ->
        "#{__MODULE__}: failed to create refresh token store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

        {:error, "#{inspect(e)}"}
    end
  catch
    :exit, e ->
      bucket_type = opts[:bucket_type] || raise "Missing bucket type"
      bucket_name = opts[:bucket_name] || "refresh_token"

      "#{__MODULE__}: failed to create refresh token store `#{bucket_name}` " <>
        "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

      {:error, "#{inspect(e)}"}
  end

  @impl true

  def start_link(opts) do
    opts = Keyword.merge([purge_interval: 1200], opts)

    # we launch the process anyway because we need to return a process
    # but the singleton will do nothing if the value is `:no_purge`
    Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)
  end

  @impl true

  def get(refresh_token_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "refresh_token"

    case Riak.find(bucket_type, bucket_name, refresh_token_id) do
      res when not is_nil(res) ->
        refresh_token =
          res
          |> Riak.CRDT.Map.get(:register, "refresh_token_data_binary")
          |> Base.decode64!(padding: false)
          |> :erlang.binary_to_term()

        Logger.debug(
          "#{__MODULE__}: getting refresh token `#{refresh_token_id}`, " <>
            "value: `#{inspect(refresh_token)}`"
        )

        {:ok, refresh_token}

      nil ->
        Logger.debug(
          "#{__MODULE__}: getting refresh token `#{refresh_token_id}`, " <> "value: `nil`"
        )

        {:ok, nil}
    end
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def get_from_subject_id(sub, opts) do
    search("sub_register:\"#{String.replace(sub, "\"", "\\\"")}\"", opts)
  end

  @impl true

  def get_from_client_id(client_id, opts) do
    search("client_id_register:\"#{String.replace(client_id, "\"", "\\\"")}\"", opts)
  end

  @impl true

  def get_from_device_id(device_id, opts) do
    search("device_id_register:\"#{String.replace(device_id, "\"", "\\\"")}\"", opts)
  end

  @impl true

  def get_from_authenticated_session_id(as_id, opts) do
    search("authenticated_session_id_register:\"#{String.replace(as_id, "\"", "\\\"")}\"", opts)
  end

  @impl true

  def put(refresh_token, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "refresh_token"

    riak_map = Riak.CRDT.Map.new()

    refresh_token_data_binary =
      refresh_token
      |> :erlang.term_to_binary()
      |> Base.encode64(padding: false)
      |> Riak.CRDT.Register.new()

    riak_map = Riak.CRDT.Map.put(riak_map, "refresh_token_data_binary", refresh_token_data_binary)

    riak_map =
      if refresh_token.data["exp"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "exp_int",
          Riak.CRDT.Register.new(to_string(refresh_token.data["exp"]))
        )
      else
        Logger.warn(
          "Inserting refresh token with no expiration: #{String.slice(refresh_token.id, 1..5)}..."
        )

        riak_map
      end

    riak_map =
      if refresh_token.data["sub"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "sub",
          Riak.CRDT.Register.new(to_string(refresh_token.data["sub"]))
        )
      else
        riak_map
      end

    riak_map =
      if refresh_token.data["client_id"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "client_id",
          Riak.CRDT.Register.new(to_string(refresh_token.data["client_id"]))
        )
      else
        riak_map
      end

    riak_map =
      if refresh_token.data["device_id"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "device_id",
          Riak.CRDT.Register.new(to_string(refresh_token.data["device_id"]))
        )
      else
        riak_map
      end

    riak_map =
      if refresh_token.data["authenticated_session_id"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "authenticated_session_id",
          Riak.CRDT.Register.new(to_string(refresh_token.data["authenticated_session_id"]))
        )
      else
        riak_map
      end

    Riak.update(riak_map, bucket_type, bucket_name, refresh_token.id)

    Logger.debug(
      "#{__MODULE__}: stored refresh token `#{refresh_token.id}`, " <>
        "value: `#{inspect(refresh_token)}`"
    )

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def delete(refresh_token_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "refresh_token"

    Riak.delete(bucket_type, bucket_name, refresh_token_id)

    Logger.debug("#{__MODULE__}: deleted refresh token `#{refresh_token_id}`")

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @doc """
  Searches in Riak-stored refresh tokens

  This function is used internaly and made available for user convenience. Refresh tokens are
  stored in the following fields:

  |               Field name           |  Indexed as   |
  |------------------------------------|:-------------:|
  | refresh_token_data_binary_register | *not indexed* |
  | exp_int_register                   | int           |
  | sub_register                       | string        |
  | client_id_register                 | string        |
  | device_id_register                 | string        |
  | authenticated_session_id_register  | string        |

  Note that you are responsible for escaping values accordingly with Solr escaping.

  ## Example

  ```elixir
  iex(13)> Asteroid.ObjectStore.RefreshToken.Riak.search("sub_register:j* AND exp_int_register:[0 TO #{
    :os.system_time(:seconds)
  }]", opts)
  {:ok, ["7WRQL4EAKW27C5BEFF3JDGXBTA", "WCJBCL7SC2THS7TSRXB2KZH7OQ"]}
  ```
  """

  @spec search(String.t(), Asteroid.ObjectStore.RefreshToken.opts()) ::
          {:ok, [Asteroid.Token.RefreshToken.id()]}
          | {:error, any()}

  def search(search_query, opts) do
    case Riak.Search.query(index_name(), search_query, rows: opts[:rows] || 1_000_000) do
      {:ok, {:search_results, result_list, _, _}} ->
        {:ok,
         for {_index_name, attribute_list} <- result_list do
           :proplists.get_value("_yz_rk", attribute_list)
         end}

      {:error, _} = error ->
        error
    end
  end

  @spec schema_name() :: String.t()

  defp schema_name(), do: "asteroid_object_store_refresh_token_riak_schema"

  @doc false

  @spec index_name() :: String.t()

  def index_name(), do: "asteroid_object_store_refresh_token_riak_index"
end


<|start_filename|>config/prod.exs<|end_filename|>
use Mix.Config

# For production, don't forget to configure the url host
# to something meaningful, Phoenix uses this information
# when generating URLs.
#
# Note we also include the path to a cache manifest
# containing the digested version of static files. This
# manifest is generated by the `mix phx.digest` task,
# which you should run after static files are built and
# before starting your production server.
config :asteroid, AsteroidWeb.Endpoint,
  http: [:inet6, port: System.get_env("PORT") || 443],
  url: [host: "example.com", port: 443, scheme: "https"],
  force_ssl: [rewrite_on: [:x_forwarded_proto]],
  server: true,
  cache_static_manifest: "priv/static/cache_manifest.json"

# Do not print debug messages in production
config :logger, level: :info

# ## SSL Support
#
# To get SSL working, you will need to add the `https` key
# to the previous section and set your `:url` port to 443:
#
#     config :asteroid, AsteroidWeb.Endpoint,
#       ...
#       url: [host: "example.com", port: 443],
#       https: [
#         :inet6,
#         port: 443,
#         cipher_suite: :strong,
#         keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
#         certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
#       ]
#
# The `cipher_suite` is set to `:strong` to support only the
# latest and more secure SSL ciphers. This means old browsers
# and clients may not be supported. You can set it to
# `:compatible` for wider support.
#
# `:keyfile` and `:certfile` expect an absolute path to the key
# and cert in disk or a relative path inside priv, for example
# "priv/ssl/server.key". For all supported SSL configuration
# options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
#
# We also recommend setting `force_ssl` in your endpoint, ensuring
# no data is ever sent via http, always redirecting to https:
#
#     config :asteroid, AsteroidWeb.Endpoint,
#       force_ssl: [hsts: true]
#
# Check `Plug.SSL` for all available options in `force_ssl`.

# ## Using releases (distillery)
#
# If you are doing OTP releases, you need to instruct Phoenix
# to start the server for all endpoints:
#
#     config :phoenix, :serve_endpoints, true
#
# Alternatively, you can configure exactly which server to
# start per endpoint:
#
#     config :asteroid, AsteroidWeb.Endpoint, server: true
#
# Note you can't rely on `System.get_env/1` when using releases.
# See the releases documentation accordingly.

config :phoenix, :json_library, Jason

# Hammer is used for cache in some plugs (rate-limiting) and for the OAuth2 device flow

config :hammer,
  backend: {Hammer.Backend.ETS, [expiry_ms: 60_000 * 60 * 4, cleanup_interval_ms: 60_000 * 10]}

# to use Riak uncomment and configure the following lines

# config :pooler,
#  pools: [
#    [
#      name: :riak,
#      group: :riak,
#      max_count: 10,
#      init_count: 5,
#      start_mfa: {Riak.Connection, :start_link, ['127.0.0.1', 8087]}
#    ]
#  ]

######################################################################
######################################################################
################## Asteroid configuration ############################
######################################################################
######################################################################

####################### Token stores #################################

config :asteroid, :object_store_access_token, module: Asteroid.ObjectStore.AccessToken.Mnesia

config :asteroid, :object_store_refresh_token, module: Asteroid.ObjectStore.RefreshToken.Mnesia

config :asteroid, :object_store_authorization_code,
  module: Asteroid.ObjectStore.AuthorizationCode.Mnesia

config :asteroid, :object_store_authenticated_session,
  module: Asteroid.ObjectStore.AuthenticatedSession.Mnesia

config :asteroid, :object_store_authentication_event,
  module: Asteroid.ObjectStore.AuthenticationEvent.Mnesia

# uncomment to enable the device authorization flow

# config :asteroid, :object_store_device_code, [
#  module: Asteroid.ObjectStore.DeviceCode.Mnesia
# ]

config :asteroid,
       :object_store_refresh_token_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_access_token_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authorization_code_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authenticated_session_before_store_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :object_store_authentication_event_before_store_callback,
       &Asteroid.Utils.id_first_param/2

####################### Attribute repositories #######################

# defaults to Mnesia in-memory backend
# 
# To enable persistence, add the current node (or another) to the `disc_copies` mnesia option:
#
#   init_opts: [instance: :client, mnesia_config: [disc_copies: [node()]]],
#
# However, before doing that, make sure you understand that it stores sensitive information on
# the disc, such as personal user informations and application passwords, which must be properly
# protected.

config :asteroid, :attribute_repositories,
  subject: [
    module: AttributeRepositoryMnesia,
    init_opts: [instance: :subject],
    run_opts: [instance: :subject]
  ],
  client: [
    module: AttributeRepositoryMnesia,
    init_opts: [instance: :client],
    run_opts: [instance: :client]
  ],
  device: [
    module: AttributeRepositoryMnesia,
    init_opts: [instance: :device],
    run_opts: [instance: :device]
  ]

####################### API plugs ###################################

config :asteroid, :browser_plugs, []

config :asteroid, :api_oauth2_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3}
  # uncomment the above `#` and the following lines to enable client_secret_post client
  # authentication for all OAuth2 endpoints
  # {APIacAuthClientSecretPost,
  #  realm: "Asteroid",
  #  callback: &Asteroid.Utils.always_nil/2,
  #  set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
  #  error_response_verbosity: :debug}
]

config :asteroid, :api_oauth2_endpoint_token_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3}
  # uncomment the following lines to enable CORS on the /api/oauth2/token endpoint
  # {Corsica, [origins: "*"]},
  # uncomment the following line to enable throttling for public clients on the
  # /api/oauth2/token endpoint
  # {APIacFilterThrottler,
  #  key: &APIacFilterThrottler.Functions.throttle_by_ip_path/1,
  #  scale: 60_000,
  #  limit: 50,
  #  exec_cond: &Asteroid.Utils.conn_not_authenticated?/1,
  #  error_response_verbosity: :debug},
]

config :asteroid, :api_oauth2_endpoint_introspect_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3}
]

config :asteroid, :api_oauth2_endpoint_revoke_plugs, [
  {APIacAuthBasic,
   realm: "Asteroid",
   callback: &Asteroid.OAuth2.Client.get_client_secret/2,
   set_error_response: &APIacAuthBasic.save_authentication_failure_response/3}
  # uncomment the following lines to enable CORS on the /api/oauth2/token endpoint
  # {Corsica, [origins: "*"]}
]

config :asteroid, :api_oauth2_endpoint_register_plugs, []

config :asteroid, :api_oauth2_endpoint_device_authorization_plugs, []

config :asteroid, :api_oidc_plugs, []

config :asteroid, :api_oidc_endpoint_userinfo_plugs, [
  {Corsica, [origins: "*"]},
  {APIacAuthBearer,
   realm: "Asteroid",
   bearer_validator: {Asteroid.OAuth2.APIacAuthBearer.Validator, []},
   bearer_extract_methods: [:header, :body],
   forward_bearer: true,
   error_response_verbosity: :normal}
]

config :asteroid, :api_request_object_plugs, []

config :asteroid, :discovery_plugs, []

config :asteroid, :well_known_plugs, []

####################### Crypto configuration #########################

config :asteroid, :crypto_keys, %{}

config :asteroid, :crypto_keys_cache, {Asteroid.Crypto.Key.Cache.ETS, []}

####################### OAuth2 general configuration ################

config :asteroid, :oauth2_grant_types_enabled, [
  :authorization_code
  # :implicit,
  # :password,
  # :client_credentials,
  # :refresh_token,
  # :"urn:ietf:params:oauth:grant-type:device_code"
]

config :asteroid, :oauth2_response_types_enabled, [
  :code
  # :token
]

config :asteroid, :oauth2_response_mode_policy, :oidc_only

config :asteroid, :api_error_response_verbosity, :normal

config :asteroid, :oauth2_scope_callback, &Asteroid.OAuth2.Scope.grant_for_flow/2

config :asteroid, :oauth2_access_token_lifetime, 60 * 10

config :asteroid, :oauth2_access_token_lifetime_callback, &Asteroid.Token.AccessToken.lifetime/1

config :asteroid, :oauth2_refresh_token_lifetime, 60 * 60

config :asteroid, :oauth2_refresh_token_lifetime_callback, &Asteroid.Token.RefreshToken.lifetime/1

config :asteroid, :oauth2_authorization_code_lifetime, 60

config :asteroid,
       :oauth2_authorization_code_lifetime_callback,
       &Asteroid.Token.AuthorizationCode.lifetime/1

config :asteroid, :oauth2_issue_refresh_token_init, true

config :asteroid, :oauth2_issue_refresh_token_init, false

config :asteroid,
       :oauth2_issue_refresh_token_callback,
       &Asteroid.Token.RefreshToken.issue_refresh_token?/1

config :asteroid,
       :oauth2_access_token_serialization_format_callback,
       &Asteroid.Token.AccessToken.serialization_format/1

config :asteroid,
       :oauth2_access_token_signing_key_callback,
       &Asteroid.Token.AccessToken.signing_key/1

config :asteroid,
       :oauth2_access_token_signing_alg_callback,
       &Asteroid.Token.AccessToken.signing_alg/1

####################### OAuth2 grant types ###########################

# ROPC

config :asteroid,
       :oauth2_endpoint_token_grant_type_password_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_password_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# refresh token

config :asteroid,
       :oauth2_endpoint_token_grant_type_refresh_token_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_refresh_token_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

####################### OAuth2 endpoints #############################

# authorize

config :asteroid,
       :oauth2_endpoint_authorize_before_send_redirect_uri_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_authorize_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :web_authorization_callback, &Asteroid.WebFlow.web_authorization_callback/2

# token

config :asteroid,
       :oauth2_endpoint_token_grant_type_client_credentials_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_client_credentials_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_authorization_code_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_authorization_code_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_auth_methods_supported_callback,
       &Asteroid.OAuth2.Endpoint.token_endpoint_auth_methods_supported/0

# introspect

config :asteroid,
       :oauth2_endpoint_introspect_client_authorized,
       &Asteroid.OAuth2.Client.endpoint_introspect_authorized?/1

config :asteroid, :oauth2_endpoint_introspect_claims_resp, [
  "scope",
  "client_id",
  "username",
  "token_type",
  "exp",
  "iat",
  "nbf",
  "sub",
  "aud",
  "iss",
  "jti"
]

config :asteroid,
       :oauth2_endpoint_introspect_claims_resp_callback,
       &Asteroid.OAuth2.Introspect.endpoint_introspect_claims_resp/1

config :asteroid,
       :oauth2_endpoint_introspect_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_introspect_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# revoke

config :asteroid,
       :oauth2_endpoint_revoke_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

# dynamic client registration

config :asteroid,
       :oauth2_endpoint_register_authorization_callback,
       &Asteroid.OAuth2.Register.request_authorized?/2

config :asteroid, :oauth2_endpoint_register_authorization_policy, :authorized_clients

config :asteroid, :oauth2_endpoint_register_additional_metadata_field, []

config :asteroid,
       :oauth2_endpoint_register_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_client_before_save_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_register_gen_client_id_callback,
       &Asteroid.OAuth2.Register.generate_client_id/2

config :asteroid,
       :oauth2_endpoint_register_gen_client_resource_id_callback,
       &Asteroid.OAuth2.Register.generate_client_resource_id/2

config :asteroid,
       :oauth2_endpoint_register_client_type_callback,
       &Asteroid.OAuth2.Register.client_type/1

# metadata

config :asteroid, :oauth2_endpoint_metadata_before_send_resp_callback, &Asteroid.Utils.id/1

config :asteroid, :oauth2_endpoint_metadata_before_send_conn_callback, &Asteroid.Utils.id/1

# discovery

config :asteroid, :oauth2_endpoint_discovery_keys_before_send_resp_callback, &Asteroid.Utils.id/1

config :asteroid, :oauth2_endpoint_discovery_keys_before_send_conn_callback, &Asteroid.Utils.id/1

####################### OAuth2 flows #################################

# ROPC

# 1 week
config :asteroid, :oauth2_flow_ropc_refresh_token_lifetime, 60 * 60 * 24 * 7

# client credentials

config :asteroid, :oauth2_flow_client_credentials_issue_refresh_token_init, false

# authorization code

# 1 week
config :asteroid, :oauth2_flow_authorization_code_refresh_token_lifetime, 3600 * 24 * 7

# implicit

config :asteroid,
       :oauth2_endpoint_token_grant_type_device_code_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_endpoint_token_grant_type_device_code_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oauth2_flow_device_authorization_rate_limiter,
       {Asteroid.OAuth2.DeviceAuthorization.RateLimiter.Hammer, []}

config :asteroid, :oauth2_flow_device_authorization_rate_limiter_interval, 5

# pkce

config :asteroid, :oauth2_pkce_policy, :optional

config :asteroid, :oauth2_pkce_allowed_methods, [:S256]

config :asteroid, :oauth2_pkce_must_use_callback, &Asteroid.OAuth2.Client.must_use_pkce?/1

####################### Scope configuration ##########################

config :asteroid, :scope_config, []

config :asteroid, :oauth2_scope_config, []

config :asteroid, :oauth2_flow_authorization_code_scope_config, []

config :asteroid, :oauth2_flow_implicit_scope_config, []

config :asteroid, :oauth2_flow_client_credentials_scope_config, []

config :asteroid, :oauth2_flow_ropc_scope_config, []

config :asteroid, :oauth2_flow_device_authorization_scope_config, []

####################### OIDC general configuration ################

config :asteroid, :oidc_id_token_lifetime, 60

config :asteroid, :oidc_id_token_lifetime_callback, &Asteroid.Token.IDToken.lifetime/1

config :asteroid, :token_id_token_before_serialize_callback, &Asteroid.Utils.id_first_param/2

config :asteroid, :oidc_issue_id_token_on_refresh_callback, false

config :asteroid,
       :oidc_issue_id_token_on_refresh_callback,
       &Asteroid.Token.IDToken.issue_id_token?/1

config :asteroid, :oidc_subject_identifier_callback, &Asteroid.OIDC.subject_identifier/2

config :asteroid,
       :oidc_subject_identifier_pairwise_salt,
       Base.encode64(:crypto.strong_rand_bytes(24))

# userinfo

config :asteroid,
       :oidc_endpoint_userinfo_before_send_resp_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid,
       :oidc_endpoint_userinfo_before_send_conn_callback,
       &Asteroid.Utils.id_first_param/2

config :asteroid, :oidc_endpoint_userinfo_signature_alg_values_supported, ["RS256"]

config :asteroid, :oidc_endpoint_userinfo_encryption_alg_values_supported, []

config :asteroid, :oidc_endpoint_userinfo_encryption_enc_values_supported, []

config :asteroid, :oidc_claims_supported, ["email", "phone_number", "gender"]


<|start_filename|>test/asteroid_web/controllers/authorize_controller_oidc_test.exs<|end_filename|>
defmodule AsteroidWeb.AuthorizeControllerOIDCTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias Asteroid.OIDC.AuthenticatedSession
  alias Asteroid.OIDC.AuthenticationEvent
  alias Asteroid.Subject
  alias OAuth2Utils.Scope

  setup_all do
    rsa_enc_alg_all =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:enc)

    Client.gen_new(id: "client_oidc_azcode_sig")
    |> Client.add("client_id", "client_oidc_azcode_sig")
    |> Client.add("client_secret", "password1")
    |> Client.add("client_type", "confidential")
    |> Client.add("grant_types", ["authorization_code", "refresh_token"])
    |> Client.add("response_types", ["code"])
    |> Client.add("scope", ["openid"])
    |> Client.add("redirect_uris", ["https://www.example.com"])
    |> Client.add("id_token_signed_response_alg", "RS384")
    |> Client.add(
      "jwks",
      [rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)]
    )
    |> Client.add("default_acr_values", ["loa2"])
    |> Client.store()

    Client.gen_new(id: "client_oidc_azcode_enc")
    |> Client.add("client_id", "client_oidc_azcode_enc")
    |> Client.add("client_secret", "password1")
    |> Client.add("client_type", "confidential")
    |> Client.add("grant_types", ["authorization_code"])
    |> Client.add("redirect_uris", ["https://www.example.com"])
    |> Client.add("id_token_signed_response_alg", "RS384")
    |> Client.add("id_token_encrypted_response_alg", "RSA1_5")
    |> Client.add("id_token_encrypted_response_enc", "A128GCM")
    |> Client.add(
      "jwks",
      [rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)]
    )
    |> Client.store()

    Client.gen_new(id: "client_oidc_pairwise")
    |> Client.add("client_id", "client_oidc_pairwise")
    |> Client.add("client_secret", "password1")
    |> Client.add("client_type", "confidential")
    |> Client.add("grant_types", ["authorization_code"])
    |> Client.add("redirect_uris", ["https://www.example.com"])
    |> Client.add("id_token_signed_response_alg", "RS384")
    |> Client.add("subject_type", "pairwise")
    |> Client.add("sector_identifier_uri", "https://example.com/sector")
    |> Client.store()

    %{rsa_enc_alg_all: rsa_enc_alg_all}
  end

  # invalid request to /authorize

  test "Error - implicit - missing nonce", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "id_token",
      "scope" => "openid"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "invalid_request"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "missing parameter"
  end

  test "Error - acr value not in config (claims parameter)", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "claims" =>
        Jason.encode!(%{
          "id_token" => %{"acr" => %{"essential" => true, "values" => ["non_existent_acr"]}}
        })
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "invalid_request"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "invalid parameter `claims`"
  end

  test "Error - acr value not in config (acr_values parameter)", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "acr_values" => "non_existent_acr"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "invalid_request"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "invalid parameter `acr_values`"
  end

  # valid request to /authorize

  test "Success - az code - simple request", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid"
    }

    Process.put(
      :oidc_flow_authorization_code_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["flow"] == "oidc_authorization_code"
    assert response["redirect_uri"] == params["redirect_uri"]
    assert response["client_id"] == params["client_id"]
    assert response["response_type"] == params["response_type"]

    assert Scope.Set.equal?(
             Scope.Set.new(response["requested_scopes"]),
             Scope.Set.new(params["scope"])
           )
  end

  test "Success - implicit - simple request", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "id_token",
      "scope" => "openid",
      "nonce" => "some_nonce_dfeasjgfndyxcrgfds"
    }

    Process.put(
      :oidc_flow_implicit_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["flow"] == "oidc_implicit"
    assert response["redirect_uri"] == params["redirect_uri"]
    assert response["client_id"] == params["client_id"]
    assert response["response_type"] == params["response_type"]

    assert Scope.Set.equal?(
             Scope.Set.new(response["requested_scopes"]),
             Scope.Set.new(params["scope"])
           )
  end

  test "Success - az code - acr requested in claims values (essential)", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "claims" =>
        Jason.encode!(%{"id_token" => %{"acr" => %{"essential" => true, "values" => ["loa1"]}}})
    }

    Process.put(
      :oidc_flow_authorization_code_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["preferred_acr"] == "loa1"
  end

  test "Success - az code - acr requested in claims values (non essential)", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "claims" => Jason.encode!(%{"id_token" => %{"acr" => %{"values" => ["loa1"]}}})
    }

    Process.put(
      :oidc_flow_authorization_code_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["preferred_acr"] == "loa1"
  end

  test "Success - az code - acr requested in claims value (essential)", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "claims" =>
        Jason.encode!(%{"id_token" => %{"acr" => %{"essential" => true, "value" => "loa1"}}})
    }

    Process.put(
      :oidc_flow_authorization_code_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["preferred_acr"] == "loa1"
  end

  test "Success - az code - acr requested in claims value (non essential)", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "claims" => Jason.encode!(%{"id_token" => %{"acr" => %{"value" => "loa1"}}})
    }

    Process.put(
      :oidc_flow_authorization_code_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["preferred_acr"] == "loa1"
  end

  test "Success - az code - acr requested in acr_values", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid",
      "acr_values" => "loa2"
    }

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["preferred_acr"] == "loa2"
  end

  test "Success - az code - acr selected from client configuration", %{conn: conn} do
    params = %{
      "client_id" => "client_oidc_azcode_sig",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "scope" => "openid"
    }

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["preferred_acr"] == "loa2"
  end

  # request back from workflow

  test "Success - az code - az code returned", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"
    assert %{"code" => az_code} = URI.decode_query(URI.parse(redirected_to(conn)).query)

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_oidc_azcode_sig"
    assert authorization_code.data["sub"] == "user_1"
  end

  test "Success - implicit - id_token returned", %{conn: conn} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      claims: %{
        "id_token" => %{"email" => nil, "phone_number" => nil, "non_standard_claim_1" => nil},
        "userinfo" => %{"nickname" => nil}
      },
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["nickname"] == nil
    assert id_token_data["email"] == "<EMAIL>"
    assert id_token_data["phone_number"] == "+3942390027"
    assert id_token_data["non_standard_claim_1"] == "some value"
  end

  test "Success - implicit - id_token returned, claim scope granted", %{conn: conn} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(["email", "phone"])
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["nickname"] == nil
    assert id_token_data["email"] == "<EMAIL>"
    assert id_token_data["phone_number"] == "+3942390027"
    refute Map.has_key?(id_token_data, "non_standard_claim_1")
  end

  test "Success - implicit - encrypted id_token returned",
       %{conn: conn, rsa_enc_alg_all: rsa_enc_alg_all} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_azcode_enc",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jwe,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {id_token_jws, _jwe} = JOSE.JWE.block_decrypt(rsa_enc_alg_all, id_token_jwe)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_enc"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
  end

  test "Success - implicit - id_token & token returned", %{conn: conn} do
    Process.put(:oidc_flow_implicit_access_token_lifetime, 30)
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :"id_token token",
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "access_token" => access_token_param,
             "token_type" => "bearer",
             "expires_in" => expires_in,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token_param)

    assert expires_in |> Integer.parse() |> elem(0) >= 29
    assert expires_in |> Integer.parse() |> elem(0) <= 31
    assert access_token.data["client_id"] == "client_oidc_azcode_sig"
    assert access_token.data["sub"] == "user_1"
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == TestOIDCHelpers.token_hash(digest, access_token_param)
  end

  test "Success - hybrid - code & id_token & token returned", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_access_token_lifetime, 30)
    Process.put(:oidc_flow_hybrid_authorization_code_lifetime, 30)
    Process.put(:oidc_flow_hybrid_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_hybrid,
      response_type: :"code id_token token",
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      claims: %{
        "id_token" => %{"email" => nil, "phone_number" => nil, "non_standard_claim_1" => nil},
        "userinfo" => %{"nickname" => nil}
      },
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "code" => az_code,
             "id_token" => id_token_jws,
             "access_token" => access_token_param,
             "token_type" => "bearer",
             "expires_in" => expires_in,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_oidc_azcode_sig"
    assert authorization_code.data["sub"] == "user_1"
    assert authorization_code.data["exp"] >= now() + 29
    assert authorization_code.data["exp"] <= now() + 31

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token_param)

    assert expires_in |> Integer.parse() |> elem(0) >= 29
    assert expires_in |> Integer.parse() |> elem(0) <= 31
    assert access_token.data["client_id"] == "client_oidc_azcode_sig"
    assert access_token.data["sub"] == "user_1"
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["at_hash"] == TestOIDCHelpers.token_hash(digest, access_token_param)
    assert id_token_data["c_hash"] == TestOIDCHelpers.token_hash(digest, az_code)
    assert id_token_data["nickname"] == nil
    assert id_token_data["email"] == "<EMAIL>"
    assert id_token_data["phone_number"] == "+3942390027"
    assert id_token_data["non_standard_claim_1"] == "some value"
  end

  test "Success - hybrid - code & id_token returned", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_authorization_code_lifetime, 30)
    Process.put(:oidc_flow_hybrid_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_hybrid,
      response_type: :"code id_token",
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "code" => az_code,
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_oidc_azcode_sig"
    assert authorization_code.data["sub"] == "user_1"
    assert authorization_code.data["exp"] >= now() + 29
    assert authorization_code.data["exp"] <= now() + 31

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, %JOSE.JWS{alg: {_alg, digest}}} =
             JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["c_hash"] == TestOIDCHelpers.token_hash(digest, az_code)
  end

  test "Success - hybrid - code & token returned", %{conn: conn} do
    Process.put(:oidc_flow_hybrid_access_token_lifetime, 30)
    Process.put(:oidc_flow_hybrid_authorization_code_lifetime, 30)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_hybrid,
      response_type: :"code token",
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "code" => az_code,
             "access_token" => access_token_param,
             "token_type" => "bearer",
             "expires_in" => expires_in,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_oidc_azcode_sig"
    assert authorization_code.data["sub"] == "user_1"
    assert authorization_code.data["exp"] >= now() + 29
    assert authorization_code.data["exp"] <= now() + 31

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token_param)

    assert expires_in |> Integer.parse() |> elem(0) >= 29
    assert expires_in |> Integer.parse() |> elem(0) <= 31
    assert access_token.data["client_id"] == "client_oidc_azcode_sig"
    assert access_token.data["sub"] == "user_1"
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31
  end

  test "Success - implicit - id_token returned with pairwise sub", %{conn: conn} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_pairwise",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_pairwise"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] != "user_1"
  end

  test "Success - implicit - id_token returned with acr set", %{conn: conn} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    {:ok, as} =
      AuthenticatedSession.gen_new("user_1")
      |> AuthenticatedSession.put_value("current_acr", "urn:example:loa:loa1")
      |> AuthenticatedSession.store()

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(),
          authenticated_session_id: as.id
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["acr"] == "urn:example:loa:loa1"
  end

  test "Success - implicit - id_token returned with auth events", %{conn: conn} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    Process.put(:oidc_acr_config,
      "2-factor": [
        callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
        auth_event_set: [["password", "otp"], ["password", "<PASSWORD>"], ["webauthn", "otp"]]
      ],
      "1-factor": [
        callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
        auth_event_set: [["password"], ["webauthn"]],
        default: true
      ]
    )

    {:ok, as} =
      AuthenticatedSession.gen_new("user_1")
      |> AuthenticatedSession.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "password")
    |> AuthenticationEvent.put_value("amr", "pwd")
    |> AuthenticationEvent.put_value("time", 100_000)
    |> AuthenticationEvent.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "otp")
    |> AuthenticationEvent.put_value("amr", "otp")
    |> AuthenticationEvent.put_value("time", 200_000)
    |> AuthenticationEvent.store()

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(),
          authenticated_session_id: as.id,
          #
          acr: "test_acr",
          # should be ignored
          amr: ["test_amr"],
          #
          auth_time: 500_000
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["acr"] == "2-factor"
    assert Enum.sort(id_token_data["amr"]) == ["otp", "pwd"]
    assert id_token_data["auth_time"] == 200_000
  end

  test "Success - implicit - id_token returned with auth info from callback", %{conn: conn} do
    Process.put(:oidc_flow_implicit_id_token_lifetime, 60)

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_implicit,
      response_type: :id_token,
      response_mode: :fragment,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      nonce: "some_nonce_dfeasjgfndyxcrgfds",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(),
          acr: "test_acr",
          amr: ["test_amr"],
          auth_time: 500_000
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "id_token" => id_token_jws,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, id_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], id_token_jws)

    id_token_data = Jason.decode!(id_token_str)

    assert id_token_data["aud"] == "client_oidc_azcode_sig"
    assert id_token_data["iss"] == OAuth2.issuer()
    assert id_token_data["nonce"] == authz_request.nonce
    assert id_token_data["sub"] == "user_1"
    assert id_token_data["acr"] == "test_acr"
    assert id_token_data["amr"] == ["test_amr"]
    assert id_token_data["auth_time"] == 500_000
  end

  test "Error - az code - loa not acceptable in ragrdes to the claims param", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :oidc_authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_oidc_azcode_sig",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      claims: %{"id_token" => %{"acr" => %{"essential" => true, "values" => ["loa2"]}}},
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(),
          acr: "loa1"
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"
    response = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert response["error"] == "access_denied"
    assert response["error_description"] =~ "requested essential acr condition not satisfied"
  end
end


<|start_filename|>test/asteroid_web/controllers/authorize_controller_jar_test.exs<|end_filename|>
defmodule AsteroidWeb.AuthorizeControllerJARTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias AsteroidWeb.Router.Helpers, as: Routes
  alias OAuth2Utils.Scope

  @base_params %{
    "response_type" => "code",
    "client_id" => "client_confidential_1",
    "redirect_uri" => "https://www.example.com",
    "scope" => "openid"
  }

  setup_all do
    Process.put(:scope_config, scopes: %{"openid" => []})

    rsa_sig_alg_all =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:sig)

    rsa_sig_alg_all_key_ops_verify =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_ops(["verify"])

    rsa_sig_alg_PS384 =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:sig)
      |> Crypto.Key.set_key_sig_alg("PS384")

    rsa_enc_alg_all =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:enc)

    rsa_enc_alg_key_ops_decrypt =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:enc)
      |> Crypto.Key.set_key_ops(["decrypt"])

    rsa_enc_alg_rsaoaep =
      JOSE.JWK.generate_key({:rsa, 1024})
      |> Crypto.Key.set_key_use(:enc)
      |> Crypto.Key.set_key_enc_alg("RSA-OAEP")

    {cache_module, cache_opts} = astrenv(:crypto_keys_cache)

    :ok = cache_module.put("rsa_enc_alg_all", rsa_enc_alg_all, cache_opts)
    :ok = cache_module.put("rsa_enc_alg_key_ops_decrypt", rsa_enc_alg_key_ops_decrypt, cache_opts)
    :ok = cache_module.put("rsa_enc_alg_rsaoaep", rsa_enc_alg_rsaoaep, cache_opts)
    # here we had client keys for testing purpose only (wront key use...)
    :ok = cache_module.put("rsa_sig_alg_all", rsa_sig_alg_all, cache_opts)

    Client.load("client_confidential_1")
    |> elem(1)
    |> Client.add("jwks", [
      rsa_sig_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1),
      rsa_sig_alg_all_key_ops_verify |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1),
      rsa_sig_alg_PS384 |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1),
      # here we had the server's enc keys just for testing purpose (signing with encryption key...
      rsa_enc_alg_all |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1),
      rsa_enc_alg_key_ops_decrypt |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1),
      rsa_enc_alg_rsaoaep |> JOSE.JWK.to_public() |> JOSE.JWK.to_map() |> elem(1)
    ])
    |> Client.store()

    %{
      "rsa_sig_alg_all" => rsa_sig_alg_all,
      "rsa_sig_alg_all_key_ops_verify" => rsa_sig_alg_all_key_ops_verify,
      "rsa_sig_alg_PS384" => rsa_sig_alg_PS384,
      "rsa_enc_alg_all" => rsa_enc_alg_all,
      "rsa_enc_alg_key_ops_decrypt" => rsa_enc_alg_key_ops_decrypt,
      "rsa_enc_alg_rsaoaep" => rsa_enc_alg_rsaoaep
    }
  end

  # success cases

  test "Success case - req obj - sig only", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn
    |> get("/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")
    |> json_response(200)
  end

  test "Success case - req obj - sig + enc", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_enc_alg_all", "RSA1_5", "A128GCM")

    conn
    |> get("/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")
    |> json_response(200)
  end

  test "Success case - req obj - sig only with alg \"none\"", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["none", "RS256"])
    JOSE.JWA.unsecured_signing(true)

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "none")

    conn
    |> get("/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")
    |> json_response(200)

    JOSE.JWA.unsecured_signing(false)
  end

  test "Success case - req uri local - sig only", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_uri_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    key = secure_random_b64()

    OAuth2.JAR.put_request_object(key, %{"request_object" => req_obj, "exp" => now() + 10})

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn
    |> get("/authorize?#{URI.encode_query(Map.put(@base_params, "request_uri", req_uri))}")
    |> json_response(200)
  end

  # error cases

  test "Error case - req obj - not supported with valid client_id / redirect_uri",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :disabled)

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "request_not_supported"
    assert params["error_description"] =~ "use of JAR request objects is disabled"
  end

  test "Error case - req obj - not supported with invalid client_id",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :disabled)

    query_params = Map.put(@base_params, "client_id", "nonexistent_client_id")

    req_obj =
      query_params
      |> Map.put("iss", "nonexistent_client_id")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(query_params, "request", req_obj))}")

    assert html_response(conn, 400) =~ "use of JAR request objects is disabled"
  end

  test "Error case - req obj - not supported with no client_id & redirect_uri",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :disabled)

    query_params =
      @base_params
      |> Map.delete("client_id")
      |> Map.delete("redirect_uri")

    req_obj =
      query_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(query_params, "request", req_obj))}")

    assert html_response(conn, 400) =~ "missing parameter"
  end

  test "Error case - req obj - oidc: missing response_type query parameter",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)

    query_params =
      @base_params
      |> Map.delete("response_type")

    req_obj =
      query_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(query_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request"
    assert params["error_description"] =~ "missing parameter"
  end

  test "Error case - req uri - not supported with valid client_id / redirect_uri",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :disabled)

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    key = secure_random_b64()

    OAuth2.JAR.put_request_object(key, %{"request_object" => req_obj, "exp" => now() + 10})

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request_uri", req_uri))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "request_uri_not_supported"
    assert params["error_description"] =~ "use of JAR request URIs is disabled"
  end

  test "Error case - req uri - not supported with invalid client_id", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :disabled)

    query_params = Map.put(@base_params, "client_id", "nonexistent_client_id")

    req_obj =
      query_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    key = secure_random_b64()

    OAuth2.JAR.put_request_object(key, %{"request_object" => req_obj, "exp" => now() + 10})

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(query_params, "request_uri", req_uri))}")

    assert html_response(conn, 400) =~ "use of JAR request URIs is disabled"
  end

  test "Error case - req uri - not supported with no redirect_uri & client_id",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :disabled)

    query_params =
      @base_params
      |> Map.delete("client_id")
      |> Map.delete("redirect_uri")

    req_obj =
      query_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    key = secure_random_b64()

    OAuth2.JAR.put_request_object(key, %{"request_object" => req_obj, "exp" => now() + 10})

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(query_params, "request_uri", req_uri))}")

    assert html_response(conn, 400) =~ "missing parameter"
  end

  test "Error case - req uri - oidc missing response_type query parameter",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_uri_only)

    query_params =
      @base_params
      |> Map.delete("response_type")

    req_obj =
      query_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    key = secure_random_b64()

    OAuth2.JAR.put_request_object(key, %{"request_object" => req_obj, "exp" => now() + 10})

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(query_params, "request_uri", req_uri))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request"
    assert params["error_description"] =~ "missing parameter"
  end

  test "Error case - req obj - invalid audience", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_verify_audience, true)

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", "https://example.com/invalid_audience")
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "invalid `aud` or `iss` JWT field"
  end

  test "Error case - req obj - invalid issuer", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_verify_audience, true)

    req_obj =
      @base_params
      |> Map.put("iss", "invalid_issuer")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "invalid `aud` or `iss` JWT field"
  end

  test "Error case - req obj - client_id differs between query param and req obj",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> Map.put("client_id", "client_confidential_1")
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    params = Map.put(@base_params, "client_id", "client_confidential_2")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"

    assert params["error_description"] =~
             "Request and request object `response_type` or `client_id` don't match"
  end

  test "Error case - req obj - response_type differs between query param and req obj",
       %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> Map.put("client_id", "client_confidential_1")
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    params = Map.put(@base_params, "response_type", "token")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"

    assert params["error_description"] =~
             "Request and request object `response_type` or `client_id` don't match"
  end

  test "Error case - req obj - invalid signature", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    [header, payload, sig] = String.split(req_obj, ".")

    # should be enough to make the sig invalid /s
    sig = String.reverse(sig)

    req_obj = Enum.join([header, payload, sig], ".")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  test "Error case - req obj - sig alg not allowed", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS512")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  test "Error case - req obj - sig alg \"none\" not allowed", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    JOSE.JWA.unsecured_signing(true)

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "none")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    JOSE.JWA.unsecured_signing(false)

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  test "Error case - req obj - sig key invalid use", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_enc_alg_all"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  test "Error case - req obj - sig key invalid key_ops", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all_key_ops_verify"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  test "Error case - req obj - sig key invalid key alg", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_PS384"], "RS256")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  # the JWS header can lie:
  # https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/

  test "Error case - req obj - sig valid but different alg than the one declared in JWS header",
       %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS384")

    [_header, payload, sig] = String.split(req_obj, ".")

    malicious_header = Base.encode64("{\"alg\":\"RS256\"}", padding: false)

    req_obj = Enum.join([malicious_header, payload, sig], ".")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWS signature verification failed"
  end

  test "Error case - req obj - encryption invalid", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_enc_alg_all", "RSA1_5", "A128GCM")

    [header, cek, iv, ciphertext, auth_tag] = String.split(req_obj, ".")

    ciphertext = String.reverse(ciphertext)

    req_obj = Enum.join([header, cek, iv, ciphertext, auth_tag], ".")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWE decryption failure"
  end

  test "Error case - req obj - enc invalid key use", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_sig_alg_all", "RSA1_5", "A128GCM")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWE decryption failure"
  end

  test "Error case - req obj - enc invalid key ops", %{conn: conn} = params do
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_enc_alg_key_ops_decrypt", "RSA1_5", "A128GCM")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWE decryption failure"
  end

  test "Error case - req obj - enc invalid alg for key", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_enc_alg_rsaoaep", "RSA1_5", "A128GCM")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWE decryption failure"
  end

  # relevance of the 2 following tests is questionnable, see:
  # https://security.stackexchange.com/questions/214185/json-web-encryption-jwe-should-one-verify-the-alg-and-enc-similarly-to-jws-al

  test "Error case - req obj - enc valid but different alg than the one declared in JWE header",
       %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    # needed for OAEP
    JOSE.JWA.crypto_fallback(true)

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_enc_alg_all", "RSA-OAEP", "A128GCM")

    JOSE.JWA.crypto_fallback(false)

    [_header, cek, iv, ciphertext, auth_tag] = String.split(req_obj, ".")

    malicious_header = Base.encode64("{\"alg\":\"RSA1_5\",\"enc\":\"A128GCM\"}", padding: false)

    req_obj = Enum.join([malicious_header, cek, iv, ciphertext, auth_tag], ".")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWE decryption failure"
  end

  test "Error case - req obj - enc valid but different enc than the one declared in JWE header",
       %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])
    Process.put(:oauth2_jar_request_object_encryption_alg_values_supported, ["RSA1_5"])
    Process.put(:oauth2_jar_request_object_encryption_enc_values_supported, ["A128GCM"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")
      |> encrypt_request_object("rsa_enc_alg_all", "RSA1_5", "A192GCM")

    [_header, cek, iv, ciphertext, auth_tag] = String.split(req_obj, ".")

    malicious_header = Base.encode64("{\"alg\":\"RSA1_5\",\"enc\":\"A128GCM\"}", padding: false)

    req_obj = Enum.join([malicious_header, cek, iv, ciphertext, auth_tag], ".")

    conn = get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request", req_obj))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_object"
    assert params["error_description"] =~ "JWE decryption failure"
  end

  test "Error case - req uri local - URI too long", %{conn: conn} = params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_uri_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    req_obj =
      @base_params
      |> Map.put("iss", "client_confidential_1")
      |> Map.put("aud", OAuth2.issuer())
      |> build_signed_request_object(params["rsa_sig_alg_all"], "RS256")

    key = String.duplicate("x", 513)

    OAuth2.JAR.put_request_object(key, %{"request_object" => req_obj, "exp" => now() + 10})

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request_uri", req_uri))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_uri"
    assert params["error_description"] =~ "`request_uri` too long"
  end

  test "Error case - req uri local - req obj doesn't exist", %{conn: conn} = _params do
    Process.put(:oidc_flow_authorization_code_web_authorization_callback, &print_json_result/2)
    Process.put(:oauth2_jar_enabled, :request_uri_only)
    Process.put(:oauth2_jar_request_object_signing_alg_values_supported, ["RS256"])

    key = secure_random_b64()

    req_uri = Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request_uri", req_uri))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_uri"
    assert params["error_description"] =~ "object could not be found"
  end

  test "Error case - req uri local - URI not https", %{conn: conn} = _params do
    Process.put(:oauth2_jar_enabled, :request_uri_only)

    req_uri = "http://www.example.com/object/afekxgasmfyskgzfxga"

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request_uri", req_uri))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_uri"
    assert params["error_description"] =~ "request URI must be HTTPS"
  end

  test "Error case - req uri local - req uri not reachable", %{conn: conn} = _params do
    Process.put(:oauth2_jar_enabled, :request_uri_only)

    req_uri = "https://www.somedomain.nonexistenttld/object/dsxfhjiwaso"

    conn =
      get(conn, "/authorize?#{URI.encode_query(Map.put(@base_params, "request_uri", req_uri))}")

    assert redirected_to(conn) =~ "https://www.example.com"

    params = URI.decode_query(URI.parse(redirected_to(conn)).query)

    assert params["error"] == "invalid_request_uri"
  end

  # helper functions

  defp print_json_result(conn, request) do
    request_map =
      Map.from_struct(request)
      |> Map.put(:client_id, request.client_id)
      |> Map.put(:requested_scopes, Scope.Set.to_list(request.requested_scopes))

    conn
    |> put_status(200)
    |> Phoenix.Controller.json(request_map)
  end

  defp build_signed_request_object(params, key, alg) do
    JOSE.JWS.sign(
      key,
      Jason.encode!(params),
      %{"alg" => alg}
    )
    |> JOSE.JWS.compact()
    |> elem(1)
  end

  defp encrypt_request_object(message, key_name, alg, enc) do
    {:ok, jwk} = Crypto.Key.get(key_name)

    JOSE.JWE.block_encrypt(jwk, message, %{"alg" => alg, "enc" => enc})
    |> JOSE.JWE.compact()
    |> elem(1)
  end
end


<|start_filename|>lib/asteroid_web/controllers/api/oauth2/device_authorization_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.DeviceAuthorizationController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias Asteroid.OAuth2
  alias Asteroid.Token.DeviceCode
  alias AsteroidWeb.Router.Helpers, as: Routes

  def handle(conn, params) do
    scope_param = params["scope"]

    with :ok <-
           Asteroid.OAuth2.grant_type_enabled?(:"urn:ietf:params:oauth:grant-type:device_code"),
         {:ok, client} <- OAuth2.Client.get_client(conn),
         :ok <-
           OAuth2.Client.grant_type_authorized?(
             client,
             "urn:ietf:params:oauth:grant-type:device_code"
           ),
         {:ok, requested_scopes} <- get_scope(scope_param),
         :ok <- OAuth2.Scope.scopes_enabled?(requested_scopes, :device_authorization),
         :ok <- OAuth2.Client.scopes_authorized?(client, requested_scopes) do
      ctx =
        %{}
        |> Map.put(:endpoint, :device_authorization)
        |> Map.put(:flow, :device_authorization)
        |> Map.put(:requested_scopes, requested_scopes)
        |> Map.put(:client, client)
        |> Map.put(:conn, conn)

      requested_scopes = astrenv(:oauth2_scope_callback).(requested_scopes, ctx)

      {:ok, device_code} =
        DeviceCode.gen_new(
          user_code: astrenv(:oauth2_flow_device_authorization_user_code_callback).(ctx)
        )
        |> DeviceCode.put_value(
          "exp",
          now() + astrenv(:oauth2_flow_device_authorization_device_code_lifetime)
        )
        |> DeviceCode.put_value("clid", client.id)
        |> DeviceCode.put_value("requested_scopes", Scope.Set.to_list(requested_scopes))
        |> DeviceCode.put_value("status", "authorization_pending")
        |> DeviceCode.store(ctx)

      verif_uri = Routes.device_url(AsteroidWeb.Endpoint, :pre_authorize)

      resp =
        %{
          "device_code" => DeviceCode.serialize(device_code),
          "user_code" => device_code.user_code,
          "verification_uri" => verif_uri,
          "verification_uri_complete" => verif_uri <> "?user_code=" <> device_code.user_code,
          "expires_in" => device_code.data["exp"] - now()
        }
        |> put_if_not_nil(
          "interval",
          astrenv(:oauth2_flow_device_authorization_rate_limiter_interval)
        )
        |> astrenv(:oauth2_endpoint_token_grant_type_password_before_send_resp_callback).(ctx)

      conn
      |> put_status(200)
      |> put_resp_header("cache-control", "no-store")
      |> put_resp_header("pragma", "no-cache")
      |> astrenv(:oauth2_endpoint_device_authorization_before_send_conn_callback).(ctx)
      |> json(resp)
    else
      {:error, %AttributeRepository.Read.NotFoundError{}} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.Client.AuthenticationError.exception(reason: :unknown_client)
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  @spec get_scope(String.t() | nil) :: {:ok, Scope.Set.t()} | {:error, Exception.t()}

  def get_scope(nil), do: {:ok, Scope.Set.new()}

  def get_scope(scope_param) do
    if Scope.oauth2_scope_param?(scope_param) do
      {:ok, Scope.Set.from_scope_param!(scope_param)}
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(
         name: "scope",
         value: scope_param
       )}
    end
  end
end


<|start_filename|>lib/asteroid/object_store/device_code/mnesia/purge.ex<|end_filename|>
defmodule Asteroid.ObjectStore.DeviceCode.Mnesia.Purge do
  @moduledoc false

  use GenServer
  require Logger

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def init(opts) do
    if opts[:purge_interval] != :no_purge do
      Process.send_after(self(), :purge, opts[:purge_interval] * 1000)
    end

    {:ok, opts}
  end

  def handle_info(:purge, opts) do
    purge(opts)

    Process.send_after(self(), :purge, opts[:purge_interval] * 1000)

    {:noreply, opts}
  end

  defp purge(opts) do
    Logger.info("#{__MODULE__}: starting device code purge process on #{node()}")

    table_name = opts[:table_name] || :asteroid_device_code

    matchspec = [
      {
        {:_, :"$1", :_, %{"exp" => :"$2"}},
        [{:<, :"$2", :os.system_time(:second)}],
        [:"$1"]
      }
    ]

    for device_code_id <- :mnesia.dirty_select(table_name, matchspec) do
      Asteroid.Token.DeviceCode.delete(device_code_id)
    end
  end
end


<|start_filename|>lib/asteroid/object_store/authentication_event/riak.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticationEvent.Riak do
  @moduledoc """
  Riak implementation of the `Asteroid.ObjectStore.AuthenticationEvent` behaviour

  ## Initializing a Riak bucket type

  ```console
  $ sudo riak-admin bucket-type create ephemeral_token '{"props":{"datatype":"map", "backend":"leveldb_mult"}}'
  ephemeral_token created

  $ sudo riak-admin bucket-type activate ephemeral_token
  ephemeral_token has been activated
  ```

  ## Options
  The options (`Asteroid.ObjectStore.AuthenticationEvent.opts()`) are:
  - `:bucket_type`: an `String.t()` for the bucket type that must be created beforehand in
  Riak. No defaults, **mandatory**
  - `bucket_name`: a `String.t()` for the bucket name. Defaults to `"authentication_event"`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `60` (1 minutes)
  - `:rows`: the maximum number of results that a search will return. Defaults to `1_000_000`.
  Search is used by the purge process.

  ## Installation function

  The `install/1` function executes the following actions:
  - it installs a custom schema (`asteroid_object_store_authentication_event_riak_schema`)
  - it creates a new index (`asteroid_object_store_authentication_event_riak_index`) on the bucket
  (and not the bucket type - so as to avoid collisions)

  This is necessary to:
  1. Efficiently index expiration timestamp
  2. Disable indexing of raw authentication event data

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Riak).
  """

  require Logger

  @behaviour Asteroid.ObjectStore.AuthenticationEvent

  @impl true

  def install(opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authentication_event"

    with :ok <-
           Riak.Search.Schema.create(
             schema_name(),
             (:code.priv_dir(:asteroid) ++ '/riak/object_store_authentication_event_schema.xml')
             |> File.read!()
           ),
         :ok <- Riak.Search.Index.put(index_name(), schema_name()),
         :ok <- Riak.Search.Index.set({bucket_type, bucket_name}, index_name()) do
      Logger.info(
        "#{__MODULE__}: created authentication event store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}`"
      )

      :ok
    else
      e ->
        "#{__MODULE__}: failed to create authentication event store `#{bucket_name}` " <>
          "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

        {:error, "#{inspect(e)}"}
    end
  catch
    :exit, e ->
      bucket_type = opts[:bucket_type] || raise "Missing bucket type"
      bucket_name = opts[:bucket_name] || "authentication_event"

      "#{__MODULE__}: failed to create authentication event store `#{bucket_name}` " <>
        "of bucket type `#{bucket_type}` (reason: #{inspect(e)})"

      {:error, "#{inspect(e)}"}
  end

  @impl true

  def start_link(opts) do
    opts = Keyword.merge([purge_interval: 60], opts)

    # we launch the process anyway because we need to return a process
    # but the singleton will do nothing if the value is `:no_purge`
    Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)
  end

  @impl true

  def get(authentication_event_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authentication_event"

    case Riak.find(bucket_type, bucket_name, authentication_event_id) do
      res when not is_nil(res) ->
        authentication_event =
          res
          |> Riak.CRDT.Map.get(:register, "authentication_event_data_binary")
          |> Base.decode64!(padding: false)
          |> :erlang.binary_to_term()

        Logger.debug(
          "#{__MODULE__}: getting authentication event `#{authentication_event_id}`, " <>
            "value: `#{inspect(authentication_event)}`"
        )

        {:ok, authentication_event}

      nil ->
        Logger.debug(
          "#{__MODULE__}: getting authentication event `#{authentication_event_id}`, " <>
            "value: `nil`"
        )

        {:ok, nil}
    end
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def get_from_authenticated_session_id(authenticated_session_id, opts) do
    search(
      "authenticated_session_id_register:\"#{
        String.replace(authenticated_session_id, "\"", "\\\"")
      }\"",
      opts
    )
  end

  @impl true

  def put(authentication_event, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authentication_event"

    riak_map = Riak.CRDT.Map.new()

    authentication_event_data_binary =
      authentication_event
      |> :erlang.term_to_binary()
      |> Base.encode64(padding: false)
      |> Riak.CRDT.Register.new()

    riak_map =
      Riak.CRDT.Map.put(
        riak_map,
        "authentication_event_data_binary",
        authentication_event_data_binary
      )

    riak_map =
      if authentication_event.authenticated_session_id != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "authenticated_session_id",
          Riak.CRDT.Register.new(authentication_event.authenticated_session_id)
        )
      else
        riak_map
      end

    riak_map =
      if authentication_event.data["exp"] != nil do
        Riak.CRDT.Map.put(
          riak_map,
          "exp_int",
          Riak.CRDT.Register.new(to_string(authentication_event.data["exp"]))
        )
      else
        Logger.warn(
          "Inserting authentication event with no expiration: #{
            String.slice(authentication_event.id, 1..5)
          }..."
        )

        riak_map
      end

    Riak.update(riak_map, bucket_type, bucket_name, authentication_event.id)

    Logger.debug(
      "#{__MODULE__}: stored authentication event `#{authentication_event.id}`, " <>
        "value: `#{inspect(authentication_event)}`"
    )

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @impl true

  def delete(authentication_event_id, opts) do
    bucket_type = opts[:bucket_type] || raise "Missing bucket type"
    bucket_name = opts[:bucket_name] || "authentication_event"

    Riak.delete(bucket_type, bucket_name, authentication_event_id)

    Logger.debug("#{__MODULE__}: deleted authentication event `#{authentication_event_id}`")

    :ok
  catch
    :exit, e ->
      {:error, "#{inspect(e)}"}
  end

  @doc """
  Searches in Riak-stored authentication events

  This function is used internaly and made available for user convenience. authentication events are
  stored in the following fields:

  |               Field name                   |  Indexed as   |
  |--------------------------------------------|:-------------:|
  | authentication_event_data_binary_register  | *not indexed* |
  | authenticated_session_id                   | string        |
  | exp_int_register                           | int           |

  Note that you are responsible for escaping values accordingly with Solr escaping.
  """

  @spec search(String.t(), Asteroid.ObjectStore.AuthenticationEvent.opts()) ::
          {:ok, [Asteroid.OIDC.AuthenticationEvent.id()]}
          | {:error, any()}

  def search(search_query, opts) do
    case Riak.Search.query(index_name(), search_query, rows: opts[:rows] || 1_000_000) do
      {:ok, {:search_results, result_list, _, _}} ->
        {:ok,
         for {_index_name, attribute_list} <- result_list do
           :proplists.get_value("_yz_rk", attribute_list)
         end}

      {:error, _} = error ->
        error
    end
  end

  @spec schema_name() :: String.t()

  defp schema_name(), do: "asteroid_object_store_authentication_event_riak_schema"

  @doc false

  @spec index_name() :: String.t()

  def index_name(), do: "asteroid_object_store_authentication_event_riak_index"
end


<|start_filename|>lib/asteroid/crypto/key.ex<|end_filename|>
defmodule Asteroid.Crypto.Key do
  @moduledoc """
  Convenience module to work with cryptographic keys
  """

  import Asteroid.Utils

  alias JOSE.JWK

  defmodule InvalidUseError do
    @moduledoc """
    Error returned when a `t:key_config_entry/0` is invalid because its `:use` is invalid

    `:use` must be one atom specified in `t:key_use`.
    """

    defexception []

    @impl true

    def message(_), do: "Invalid `:use` option (must be one of: [:sign, :enc])"
  end

  defmodule NoSuitableKeyError do
    @moduledoc """
    Error returned when no suitable key was found

    This can be returned, for instance, when trying to find an encryption key for a client that
    has published only signing keys.
    """

    defexception []

    @impl true

    def message(_), do: "no suitable key was found"
  end

  @typedoc """
  A JSON web key in its raw map format

  ## Example

  ```elixir
  %{
    "d" => "<KEY>",
    "dp" => "BwKfV3Akq5_MFZDFZ<KEY>",
    "dq" => "<KEY>",
    "e" => "AQAB",
    "kty" => "RSA",
    "n" => "<KEY>           <KEY>           <KEY>           MTBQY4uDZlxvb3qCo5ZwKh9kG4LT6_I5IhlJH7aGhyxXFvUK-DWNmoudF8\n           NAco9_h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXpoQ",
    "p" => "<KEY>",
    "q" => "<KEY>",
    "qi" => "<KEY>"
  } 
  ```
  """

  @type t :: map()

  @type name :: String.t()

  @typedoc """
  Key config options

  A key config entry can have the following values:
  - `{:pem_file, Keyword.t()}` to load a PEM file stored in the disc, and that includes
  a private key. The options of the `Keyword.t()` are:
    - `:path`: the path to the file. **Mandatory**
    - `:password`: the password of the file to decrypt it, if any
    - `:use`: a `t:key_use/0`. **Mandatory**
    - `:advertise`: a boolean to determine whether the key should be advertised on the
    jwk URI endpoint or not. Defaults to `true`
  - `{:map, Keyword.t()}`: a `JOSE.JWK` converted to a map (using for instance
  `JOSE.JWK.to_map/1`). The options of the `Keyword.t()` are:
    - `:key`: a key as returned by `JOSE.JWK.to_map/1`, for instance:
    `{%{kty: :jose_jwk_kty_oct},
    %{"k" => "P9dGnU_We5thJOOigUGtl00WmubLVAAr1kYsAUP80Sc", "kty" => "oct"}}`. **Mandatory**
    - `:use`: a `t:key_use/0`. **Mandatory**
    - `:advertise`: a boolean to determine whether the key should be advertised on the
    jwk URI endpoint or not. Defaults to `true`
  - `{:auto_gen, any()}`: configuration to automatically generated a key on startup.
  The options of the `Keyword.t()` are:
    - `params`: the parameters that will be passed to `JOSE.JWK.generate_key/1`. **Mandatory**
    - `:use`: a `t:key_use/0`. **Mandatory**
    - `:advertise`: a boolean to determine whether the key should be advertised on the
    jwk URI endpoint or not. Defaults to `true`
  """

  @type key_config_entry ::
          {:pem_file, Keyword.t()}
          | {:map, Keyword.t()}
          | {:auto_gen, Keyword.t()}

  @typedoc """
  Key config entry

  Each key requires a name and its associated config. Keys are referred and used by their
  names.
  """

  @type key_config :: %{required(name()) => key_config_entry()}

  @type key_use :: :sig | :enc

  @typedoc """
  JOSE JWS signature algorithm

  Example of output of `JOSE.JWA.supports/0`:

  ```elixir
  iex> JOSE.JWA.supports() |> Enum.find(fn {:jws, _} -> true; _ -> false end) |> elem(1)
  {:alg,
   ["ES256", "ES384", "ES512", "HS256", "HS384", "HS512", "PS256", "PS384",
    "PS512", "RS256", "RS384", "RS512"]}
  ```
  """

  @jws_alg [
    "ES256",
    "ES384",
    "ES512",
    "Ed25519",
    "Ed25519ph",
    "Ed448",
    "Ed448ph",
    "HS256",
    "HS384",
    "HS512",
    "PS256",
    "PS384",
    "PS512",
    "Poly1305",
    "RS256",
    "RS384",
    "RS512"
  ]

  @type jws_alg :: String.t()

  @typedoc """
  JOSE JWE algorithm

  Example of output of `JOSE.JWA.supports/0`:

  ```elixir
  iex> JOSE.JWA.supports() |> Enum.find(fn {:jwe, _, _, _} -> true; _ -> false end) |> elem(1)
  {:alg,
   ["A128GCMKW", "A128KW", "A192GCMKW", "A192KW", "A256GCMKW", "A256KW",
    "ECDH-ES", "ECDH-ES+A128KW", "ECDH-ES+A192KW", "ECDH-ES+A256KW",
    "PBES2-HS256+A128KW", "PBES2-HS384+A192KW", "PBES2-HS512+A256KW", "RSA1_5",
    "dir"]}
  ```
  """

  @jwe_alg [
    "A128GCMKW",
    "A128KW",
    "A192GCMKW",
    "A192KW",
    "A256GCMKW",
    "A256KW",
    "ECDH-ES",
    "ECDH-ES+A128KW",
    "ECDH-ES+A192KW",
    "ECDH-ES+A256KW",
    "PBES2-HS256+A128KW",
    "PBES2-HS384+A192KW",
    "PBES2-HS512+A256KW",
    "RSA-OAEP",
    "RSA-OAEP-256",
    "RSA1_5",
    "dir"
  ]

  @type jwe_alg :: String.t()

  @typedoc """
  JOSE JWE encryption algorithm

  Example of output of `JOSE.JWA.supports/0`:

  ```elixir
  iex> JOSE.JWA.supports() |> Enum.find(fn {:jwe, _, _, _} -> true; _ -> false end) |> elem(2)
  {:enc,
   ["A128CBC-HS256", "A128GCM", "A192CBC-HS384", "A192GCM", "A256CBC-HS512",
    "A256GCM"]}
  ```
  """

  @jwe_enc [
    "A128CBC-HS256",
    "A128GCM",
    "A192CBC-HS384",
    "A192GCM",
    "A256CBC-HS512",
    "A256GCM",
    "ChaCha20/Poly1305"
  ]

  @type jwe_enc :: String.t()

  @spec load_from_config!() :: :ok

  def load_from_config!() do
    {cache_module, cache_opts} = astrenv(:crypto_keys_cache)

    if function_exported?(cache_module, :start_link, 1) do
      cache_module.start_link(cache_opts)
    else
      if function_exported?(cache_module, :start, 1) do
        cache_module.start(cache_opts)
      end
    end

    existing_keys =
      for {key_name, _key} <- cache_module.get_all(cache_opts) do
        key_name
      end

    inserted_keys =
      for {key_name, key_config} <- astrenv(:crypto_keys, []) do
        jwk = prepare!(key_config)

        :ok = cache_module.put(key_name, jwk, cache_opts)

        key_name
      end

    for key_to_delete <- existing_keys -- inserted_keys do
      cache_module.delete(key_to_delete, cache_opts)
    end

    :ok
  end

  @spec prepare!(key_config_entry()) :: %JOSE.JWK{}

  def prepare!({:pem_file, params}) do
    if params[:use] == nil do
      raise InvalidUseError
    end

    if params[:password] do
      JWK.from_pem_file(params[:password], params[:path])
      |> set_key_use(params[:use])
      |> set_key_id()
      |> set_advertised(params[:advertise])
    else
      JWK.from_pem_file(params[:path])
      |> set_key_use(params[:use])
      |> set_key_id()
      |> set_advertised(params[:advertise])
    end
  end

  def prepare!({:map, params}) do
    if params[:use] == nil do
      raise InvalidUseError
    end

    JWK.from_map(params[:key])
    |> set_key_use(params[:use])
    |> set_key_id()
    |> set_advertised(params[:advertise])
  end

  def prepare!({:auto_gen, params}) do
    if params[:use] == nil do
      raise InvalidUseError
    end

    JWK.generate_key(params[:params])
    |> set_key_use(params[:use])
    |> set_key_id()
    |> set_advertised(params[:advertise])
  end

  @spec get(name()) :: {:ok, %JOSE.JWK{}} | {:error, Exception.t()}

  def get(key_name) do
    {cache_module, cache_opts} = astrenv(:crypto_keys_cache)

    cache_module.get(key_name, cache_opts)
  end

  @doc """
  Returns all the keys

  Note that it returns the private keys
  """

  @spec get_all() :: [%JOSE.JWK{}]

  def get_all() do
    {cache_module, cache_opts} = astrenv(:crypto_keys_cache)

    for {_key_name, jwk} <- cache_module.get_all(cache_opts) do
      JOSE.JWK.from(jwk)
    end
  end

  @doc """
  Returns all the public keys
  """

  @spec get_all_public() :: [%JOSE.JWK{}]

  def get_all_public() do
    {cache_module, cache_opts} = astrenv(:crypto_keys_cache)

    for {_key_name, jwk} <- cache_module.get_all(cache_opts) do
      JOSE.JWK.to_public(jwk)
    end
  end

  @spec set_key_use(%JOSE.JWK{}, key_use()) :: %JOSE.JWK{}

  def set_key_use(%JOSE.JWK{} = jwk, key_use) when key_use in [:sig, :enc] do
    %{jwk | fields: Map.put(jwk.fields, "use", Atom.to_string(key_use))}
  end

  @spec set_key_ops(%JOSE.JWK{}, [String.t()]) :: %JOSE.JWK{}

  def set_key_ops(%JOSE.JWK{} = jwk, key_ops) when is_list(key_ops) do
    %{jwk | fields: Map.put(jwk.fields, "key_ops", key_ops)}
  end

  @spec set_key_id(%JOSE.JWK{}) :: %JOSE.JWK{}

  def set_key_id(jwk) do
    %{jwk | fields: Map.put(jwk.fields, "kid", JOSE.JWK.thumbprint(jwk))}
  end

  @spec set_key_sig_alg(%JOSE.JWK{}, jws_alg()) :: %JOSE.JWK{}

  def set_key_sig_alg(jwk, jws_alg) when jws_alg in @jws_alg do
    %{jwk | fields: Map.put(jwk.fields, "alg", jws_alg)}
  end

  @spec set_key_enc_alg(%JOSE.JWK{}, jwe_alg()) :: %JOSE.JWK{}

  def set_key_enc_alg(jwk, jwe_alg) when jwe_alg in @jwe_alg do
    %{jwk | fields: Map.put(jwk.fields, "alg", jwe_alg)}
  end

  @spec set_key_enc_enc(%JOSE.JWK{}, jwe_enc()) :: %JOSE.JWK{}

  def set_key_enc_enc(jwk, jwe_enc) when jwe_enc in @jwe_enc do
    %{jwk | fields: Map.put(jwk.fields, "enc", jwe_enc)}
  end

  @spec set_advertised(%JOSE.JWK{}, boolean() | nil) :: %JOSE.JWK{}

  defp set_advertised(jwk, false) do
    %{jwk | fields: Map.put(jwk.fields, "advertise", false)}
  end

  defp set_advertised(jwk, _) do
    %{jwk | fields: Map.put(jwk.fields, "advertise", true)}
  end
end


<|start_filename|>lib/asteroid_web/controllers/api/oauth2/token_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.TokenController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias OAuth2Utils.Scope
  alias Asteroid.Context
  alias Asteroid.OAuth2
  alias Asteroid.Token
  alias Asteroid.Token.{RefreshToken, AccessToken, AuthorizationCode, DeviceCode, IDToken}
  alias Asteroid.{Client, Subject}

  defmodule ExceedingScopeError do
    @moduledoc """
    Error returned when the requested scopes exceed the scopes granted beforehand
    """

    @enforce_keys [:granted_scopes, :requested_scopes]

    defexception [:granted_scopes, :requested_scopes]

    @type t :: %__MODULE__{
            granted_scopes: Scope.Set.t(),
            requested_scopes: Scope.Set.t()
          }

    @impl true

    def message(%{granted_scopes: granted_scopes, requested_scopes: requested_scopes}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Requested scopes exceed granted scope " <>
            "(granted scopes: #{Scope.Set.to_list(granted_scopes)}, " <>
            "requested scopes: #{Scope.Set.to_list(requested_scopes)}, scopes in excess: " <>
            "#{Scope.Set.to_list(Scope.Set.difference(requested_scopes, granted_scopes))}"

        :normal ->
          "Requested scopes exceed granted scope"

        :minimal ->
          ""
      end
    end
  end

  # OAuth2 ROPC flow (resource owner password credentials)
  # https://tools.ietf.org/html/rfc6749#section-4.3.2

  def handle(
        conn,
        %{"grant_type" => "password", "username" => username, "password" => password} = params
      )
      when username != nil and password != nil do
    scope_param = params["scope"]

    with :ok <- Asteroid.OAuth2.grant_type_enabled?(:password),
         :ok <- valid_username_param?(username),
         :ok <- valid_password_param?(password),
         {:ok, client} <- OAuth2.Client.get_client(conn),
         :ok <- OAuth2.Client.grant_type_authorized?(client, "password"),
         {:ok, requested_scopes} <- get_scope(scope_param),
         :ok <- OAuth2.Scope.scopes_enabled?(requested_scopes, :ropc),
         :ok <- OAuth2.Client.scopes_authorized?(client, requested_scopes),
         {:ok, subject} <-
           astrenv(:oauth2_flow_ropc_username_password_verify_callback).(conn, username, password) do
      ctx =
        %{}
        |> Map.put(:endpoint, :token)
        |> Map.put(:flow, :ropc)
        |> Map.put(:grant_type, :password)
        |> Map.put(:requested_scopes, requested_scopes)
        |> Map.put(:subject, subject)
        |> Map.put(:client, client)
        |> Map.put(:conn, conn)

      granted_scopes = astrenv(:oauth2_scope_callback).(requested_scopes, ctx)

      ctx = Map.put(ctx, :granted_scopes, granted_scopes)

      maybe_refresh_token =
        if astrenv(:oauth2_issue_refresh_token_callback).(ctx) do
          # FIXME: handle {:error, reason} failure case?
          {:ok, refresh_token} =
            RefreshToken.gen_new()
            |> RefreshToken.put_value("iat", now())
            |> RefreshToken.put_value(
              "exp",
              now() + astrenv(:oauth2_refresh_token_lifetime_callback).(ctx)
            )
            |> RefreshToken.put_value("client_id", client.id)
            |> RefreshToken.put_value("sub", subject.id)
            |> RefreshToken.put_value("scope", Scope.Set.to_list(granted_scopes))
            |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
            |> RefreshToken.put_value("iss", OAuth2.issuer())
            |> RefreshToken.store(ctx)

          refresh_token
        else
          nil
        end

      access_token =
        if maybe_refresh_token do
          new_access_token(ctx, refresh_token: maybe_refresh_token.id)
        else
          new_access_token(ctx)
        end
        |> AccessToken.put_value("iat", now())
        |> AccessToken.put_value(
          "exp",
          now() + astrenv(:oauth2_access_token_lifetime_callback).(ctx)
        )
        |> AccessToken.put_value("client_id", client.id)
        |> AccessToken.put_value("sub", subject.id)
        |> AccessToken.put_value("scope", Scope.Set.to_list(granted_scopes))
        |> AccessToken.put_value("iss", OAuth2.issuer())

      # FIXME: handle failure case?
      {:ok, access_token} = AccessToken.store(access_token, ctx)

      resp =
        %{
          "access_token" => AccessToken.serialize(access_token),
          "expires_in" => access_token.data["exp"] - now(),
          "token_type" => "bearer"
        }
        |> maybe_put_refresh_token(maybe_refresh_token)
        |> put_scope_if_changed(requested_scopes, granted_scopes)
        |> astrenv(:oauth2_endpoint_token_grant_type_password_before_send_resp_callback).(ctx)

      conn
      |> put_status(200)
      |> put_resp_header("cache-control", "no-store")
      |> put_resp_header("pragma", "no-cache")
      |> astrenv(:oauth2_endpoint_token_grant_type_password_before_send_conn_callback).(ctx)
      |> json(resp)
    else
      {:error, %AttributeRepository.Read.NotFoundError{} = e} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.InvalidGrantError.exception(
            grant: "password",
            reason: "incorrect username or password",
            debug_details: Exception.message(e)
          )
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, %{"grant_type" => "password"}) do
    AsteroidWeb.Error.respond_api(
      conn,
      OAuth2.Request.InvalidRequestError.exception(
        reason: "Missing `username` or `password` parameter"
      )
    )
  end

  def handle(conn, %{"grant_type" => "client_credentials"}) do
    scope_param = conn.body_params["scope"]

    with :ok <- Asteroid.OAuth2.grant_type_enabled?(:client_credentials),
         {:ok, client} <- OAuth2.Client.get_authenticated_client(conn),
         :ok <- OAuth2.Client.grant_type_authorized?(client, "client_credentials"),
         {:ok, requested_scopes} <- get_scope(scope_param),
         :ok <- OAuth2.Scope.scopes_enabled?(requested_scopes, :client_credentials),
         :ok <- OAuth2.Client.scopes_authorized?(client, requested_scopes) do
      ctx =
        %{}
        |> Map.put(:endpoint, :token)
        |> Map.put(:flow, :client_credentials)
        |> Map.put(:grant_type, :client_credentials)
        |> Map.put(:requested_scopes, requested_scopes)
        |> Map.put(:client, client)
        |> Map.put(:conn, conn)

      granted_scopes = astrenv(:oauth2_scope_callback).(requested_scopes, ctx)

      ctx = Map.put(ctx, :granted_scopes, granted_scopes)

      maybe_refresh_token =
        if astrenv(:oauth2_issue_refresh_token_callback).(ctx) do
          # FIXME: handle {:error, reason} failure case?
          {:ok, refresh_token} =
            RefreshToken.gen_new()
            |> RefreshToken.put_value("iat", now())
            |> RefreshToken.put_value(
              "exp",
              now() + astrenv(:oauth2_refresh_token_lifetime_callback).(ctx)
            )
            |> RefreshToken.put_value("client_id", client.id)
            |> RefreshToken.put_value("scope", Scope.Set.to_list(granted_scopes))
            |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "client_credentials")
            |> RefreshToken.put_value("iss", OAuth2.issuer())
            |> RefreshToken.store(ctx)

          refresh_token
        else
          nil
        end

      access_token =
        if maybe_refresh_token do
          new_access_token(ctx, refresh_token: maybe_refresh_token.id)
        else
          new_access_token(ctx)
        end
        |> AccessToken.put_value("iat", now())
        |> AccessToken.put_value(
          "exp",
          now() + astrenv(:oauth2_access_token_lifetime_callback).(ctx)
        )
        |> AccessToken.put_value("client_id", client.id)
        |> AccessToken.put_value("scope", Scope.Set.to_list(granted_scopes))
        |> AccessToken.put_value("iss", OAuth2.issuer())

      # FIXME: handle failure case?
      {:ok, access_token} = AccessToken.store(access_token, ctx)

      resp =
        %{
          "access_token" => AccessToken.serialize(access_token),
          "expires_in" => access_token.data["exp"] - now(),
          "token_type" => "bearer"
        }
        |> maybe_put_refresh_token(maybe_refresh_token)
        |> put_scope_if_changed(requested_scopes, granted_scopes)
        |> astrenv(:oauth2_endpoint_token_grant_type_client_credentials_before_send_resp_callback).(
          ctx
        )

      conn
      |> put_status(200)
      |> put_resp_header("cache-control", "no-store")
      |> put_resp_header("pragma", "no-cache")
      |> astrenv(:oauth2_endpoint_token_grant_type_client_credentials_before_send_conn_callback).(
        ctx
      )
      |> json(resp)
    else
      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, %{"grant_type" => "refresh_token", "refresh_token" => refresh_token_param})
      when refresh_token_param != nil do
    scope_param = conn.body_params["scope"]

    with :ok <- Asteroid.OAuth2.grant_type_enabled?(:refresh_token),
         :ok <- valid_refresh_token_param?(refresh_token_param),
         {:ok, client} <- OAuth2.Client.get_client(conn),
         :ok <- OAuth2.Client.grant_type_authorized?(client, "refresh_token"),
         {:ok, requested_scopes} <- get_scope(scope_param),
         # we let this check in case of dynamic change of the client's configuration
         :ok <- OAuth2.Client.scopes_authorized?(client, requested_scopes),
         {:ok, refresh_token} <- RefreshToken.get(refresh_token_param),
         :ok <- refresh_token_granted_to_client?(refresh_token, client) do
      if Scope.Set.subset?(
           requested_scopes,
           Scope.Set.new(refresh_token.data["scope"] || [])
         ) do
        maybe_subject =
          case Subject.load_from_unique_attribute(
                 "sub",
                 refresh_token.data["sub"],
                 attributes: ["sub"]
               ) do
            {:ok, subject} ->
              subject

            _ ->
              nil
          end

        maybe_initial_flow =
          if refresh_token.data["__asteroid_oauth2_initial_flow"] do
            OAuth2.to_flow(refresh_token.data["__asteroid_oauth2_initial_flow"])
          end

        granted_scopes =
          if Scope.Set.size(requested_scopes) == 0 do
            Scope.Set.new(refresh_token.data["scope"] || [])
          else
            requested_scopes
          end

        ctx =
          %{}
          |> Map.put(:endpoint, :token)
          |> put_if_not_nil(:flow, maybe_initial_flow)
          |> Map.put(:grant_type, :refresh_token)
          |> Map.put(:requested_scopes, requested_scopes)
          |> Map.put(:granted_scopes, granted_scopes)
          |> put_if_not_nil(:subject, maybe_subject)
          |> Map.put(:client, client)
          |> put_if_not_nil(:scope, requested_scopes)
          |> Map.put(:conn, conn)

        maybe_new_refresh_token =
          if astrenv(:oauth2_issue_refresh_token_callback).(ctx) do
            :ok = RefreshToken.delete(refresh_token)

            # FIXME: handle {:error, reason} failure case?
            {:ok, new_refresh_token} =
              Enum.reduce(
                refresh_token.data,
                RefreshToken.gen_new(),
                fn
                  {key, value}, acc ->
                    RefreshToken.put_value(acc, key, value)
                end
              )
              |> RefreshToken.put_value("iat", now())
              |> RefreshToken.put_value(
                "exp",
                now() + astrenv(:oauth2_refresh_token_lifetime_callback).(ctx)
              )
              |> RefreshToken.store(ctx)

            new_refresh_token
          end

        access_token =
          if maybe_new_refresh_token do
            new_access_token(ctx, refresh_token: maybe_new_refresh_token.id)
          else
            new_access_token(ctx, refresh_token: refresh_token.id)
          end
          |> AccessToken.put_value("iat", now())
          |> AccessToken.put_value(
            "exp",
            now() + astrenv(:oauth2_access_token_lifetime_callback).(ctx)
          )
          |> AccessToken.put_value("client_id", client.id)
          |> AccessToken.put_value("sub", if(maybe_subject, do: maybe_subject.attrs["sub"]))
          |> AccessToken.put_value("scope", Scope.Set.to_list(granted_scopes))
          |> AccessToken.put_value("iss", OAuth2.issuer())
          |> AccessToken.put_value(
            "__asteroid_oidc_claims",
            refresh_token.data["__asteroid_oidc_claims"]
          )
          |> AccessToken.put_value(
            "__asteroid_oidc_authenticated_session_id",
            refresh_token.data["__asteroid_oidc_authenticated_session_id"]
          )
          |> AccessToken.put_value(
            "__asteroid_oidc_initial_acr",
            refresh_token.data["__asteroid_oidc_initial_acr"]
          )
          |> AccessToken.put_value(
            "__asteroid_oidc_initial_amr",
            refresh_token.data["__asteroid_oidc_initial_amr"]
          )
          |> AccessToken.put_value(
            "__asteroid_oidc_initial_auth_time",
            refresh_token.data["__asteroid_oidc_initial_auth_time"]
          )

        {:ok, access_token} = AccessToken.store(access_token, ctx)

        access_token_serialized = AccessToken.serialize(access_token)

        maybe_id_token_serialized =
          if maybe_initial_flow in [:oidc_authorization_code, :oidc_hybrid] and
               astrenv(:oidc_issue_id_token_on_refresh_callback).(ctx) do
            %IDToken{
              iss: OAuth2.issuer(),
              # should be nil, crashes if so
              sub: maybe_subject.attrs["sub"],
              aud: refresh_token.data["client_id"],
              exp: now() + astrenv(:oidc_id_token_lifetime_callback).(ctx),
              iat: now(),
              auth_time: refresh_token.data["__asteroid_oidc_initial_auth_time"],
              acr: refresh_token.data["__asteroid_oidc_initial_acr"],
              amr: refresh_token.data["__asteroid_oidc_initial_amr"],
              client: client
            }
            |> IDToken.add_sub_claims(
              refresh_token.data["__asteroid_oidc_claims"] || [],
              maybe_subject
            )
            |> astrenv(:token_id_token_before_serialize_callback).(ctx)
            |> IDToken.serialize()
          else
            nil
          end

        resp =
          %{
            "access_token" => access_token_serialized,
            "expires_in" => access_token.data["exp"] - now(),
            "token_type" => "bearer"
          }
          |> maybe_put_refresh_token(maybe_new_refresh_token)
          |> put_if_not_nil("id_token", maybe_id_token_serialized)
          |> put_scope_if_changed(requested_scopes, granted_scopes)
          |> astrenv(:oauth2_endpoint_token_grant_type_refresh_token_before_send_resp_callback).(
            ctx
          )

        conn
        |> put_status(200)
        |> put_resp_header("cache-control", "no-store")
        |> put_resp_header("pragma", "no-cache")
        |> astrenv(:oauth2_endpoint_token_grant_type_refresh_token_before_send_conn_callback).(
          ctx
        )
        |> json(resp)
      else
        AsteroidWeb.Error.respond_api(
          conn,
          ExceedingScopeError.exception(
            requested_scopes: requested_scopes,
            granted_scopes: Scope.Set.new(refresh_token.data["scope"] || [])
          )
        )
      end
    else
      {:error, %Token.InvalidTokenError{} = e} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.InvalidGrantError.exception(
            grant: "authorization code",
            reason: "invalid refresh token",
            debug_details: Exception.message(e)
          )
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, %{"grant_type" => "refresh_token"}) do
    AsteroidWeb.Error.respond_api(
      conn,
      OAuth2.Request.InvalidRequestError.exception(reason: "Missing `refresh_token` parameter")
    )
  end

  # authorization code

  def handle(
        conn,
        %{"grant_type" => "authorization_code", "code" => code, "redirect_uri" => redirect_uri} =
          params
      ) do
    with :ok <- Asteroid.OAuth2.grant_type_enabled?(:authorization_code),
         {:ok, client} <- OAuth2.Client.get_client(conn),
         :ok <- OAuth2.Client.grant_type_authorized?(client, "authorization_code"),
         {:ok, authz_code} <- AuthorizationCode.get(code),
         :ok <- authorization_code_granted_to_client?(authz_code, client),
         :ok <- redirect_uris_match?(authz_code, redirect_uri),
         :ok <- pkce_code_verifier_valid?(authz_code, params["code_verifier"]),
         {:ok, subject} <- Subject.load_from_unique_attribute("sub", authz_code.data["sub"]) do
      client = Client.fetch_attributes(client, ["client_id"])

      requested_scopes = Scope.Set.new(authz_code.data["requested_scopes"] || [])
      granted_scopes = Scope.Set.new(authz_code.data["granted_scopes"] || [])

      flow = OAuth2.to_flow(authz_code.data["__asteroid_oauth2_initial_flow"])

      ctx =
        %{}
        |> Map.put(:endpoint, :token)
        |> Map.put(:flow, flow)
        |> Map.put(:grant_type, :authorization_code)
        |> Map.put(:requested_scopes, requested_scopes)
        |> Map.put(:granted_scopes, granted_scopes)
        |> Map.put(:subject, subject)
        |> Map.put(:client, client)
        |> Map.put(:conn, conn)

      maybe_refresh_token =
        if astrenv(:oauth2_issue_refresh_token_callback).(ctx) do
          {:ok, refresh_token} =
            Enum.reduce(
              authz_code.data,
              RefreshToken.gen_new(),
              fn
                # FIXME: determine how to know which fields to copy, and rewrite more elegantly
                {"redirect_uri", _v}, acc ->
                  acc

                {"requested_scopes", _v}, acc ->
                  acc

                {"granted_scopes", _v}, acc ->
                  acc

                {"__asteroid_oidc_authenticated_session_id" = k, v}, acc ->
                  RefreshToken.put_value(acc, k, v)

                {"__asteroid_oidc_initial_" <> _ = k, v}, acc ->
                  RefreshToken.put_value(acc, k, v)

                {"__asteroid_oidc_claims" = k, v}, acc ->
                  RefreshToken.put_value(acc, k, v)

                {"__asteroid_oauth2_initial_flow" = k, v}, acc ->
                  RefreshToken.put_value(acc, k, v)

                {"__asteroid" <> _, _v}, acc ->
                  acc

                {k, v}, acc ->
                  RefreshToken.put_value(acc, k, v)
              end
            )
            |> RefreshToken.put_value("iat", now())
            |> RefreshToken.put_value(
              "exp",
              now() + astrenv(:oauth2_refresh_token_lifetime_callback).(ctx)
            )
            |> RefreshToken.put_value("scope", Scope.Set.to_list(granted_scopes))
            |> RefreshToken.store(ctx)

          refresh_token
        else
          nil
        end

      access_token =
        Enum.reduce(
          authz_code.data,
          if maybe_refresh_token do
            new_access_token(ctx, refresh_token: maybe_refresh_token.id)
          else
            new_access_token(ctx)
          end,
          fn
            # FIXME: determine how to know which fields to copy, and rewrite more elegantly
            {"redirect_uri", _v}, acc ->
              acc

            {"requested_scopes", _v}, acc ->
              acc

            {"granted_scopes", _v}, acc ->
              acc

            {"__asteroid_oidc_authenticated_session_id" = k, v}, acc ->
              AccessToken.put_value(acc, k, v)

            {"__asteroid_oauth2_initial_flow" = k, v}, acc ->
              AccessToken.put_value(acc, k, v)

            {"__asteroid_oidc_claims" = k, v}, acc ->
              AccessToken.put_value(acc, k, v)

            {"__asteroid" <> _, _v}, acc ->
              acc

            {k, v}, acc ->
              AccessToken.put_value(acc, k, v)
          end
        )
        |> AccessToken.put_value("iat", now())
        |> AccessToken.put_value(
          "exp",
          now() + astrenv(:oauth2_access_token_lifetime_callback).(ctx)
        )
        |> AccessToken.put_value("scope", Scope.Set.to_list(granted_scopes))

      # FIXME: handle failure case?
      {:ok, access_token} = AccessToken.store(access_token, ctx)

      access_token_serialized = AccessToken.serialize(access_token)

      maybe_id_token_serialized =
        if flow in [:oidc_authorization_code, :oidc_hybrid] do
          %IDToken{
            iss: OAuth2.issuer(),
            sub: authz_code.data["sub"],
            aud: client.attrs["client_id"],
            exp: now() + astrenv(:oidc_id_token_lifetime_callback).(ctx),
            iat: now(),
            auth_time: authz_code.data["__asteroid_oidc_initial_auth_time"],
            acr: authz_code.data["__asteroid_oidc_initial_acr"],
            amr: authz_code.data["__asteroid_oidc_initial_amr"],
            nonce: authz_code.data["__asteroid_oidc_nonce"],
            client: client,
            associated_access_token_serialized:
              if flow == :oidc_authorization_code do
                access_token_serialized
              end
          }
          |> IDToken.add_sub_claims(authz_code.data["__asteroid_oidc_claims"] || [], subject)
          |> astrenv(:token_id_token_before_serialize_callback).(ctx)
          |> IDToken.serialize()
        else
          nil
        end

      resp =
        %{
          "access_token" => access_token_serialized,
          "expires_in" => access_token.data["exp"] - now(),
          "token_type" => "bearer"
        }
        |> maybe_put_refresh_token(maybe_refresh_token)
        |> put_if_not_nil("id_token", maybe_id_token_serialized)
        |> put_scope_if_changed(requested_scopes, granted_scopes)
        |> astrenv(:oauth2_endpoint_token_grant_type_authorization_code_before_send_resp_callback).(
          ctx
        )

      conn
      |> put_status(200)
      |> put_resp_header("cache-control", "no-store")
      |> put_resp_header("pragma", "no-cache")
      |> astrenv(:oauth2_endpoint_token_grant_type_authorization_code_before_send_conn_callback).(
        ctx
      )
      |> json(resp)
    else
      {:error, %Token.InvalidTokenError{} = e} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.InvalidGrantError.exception(
            grant: "authorization code",
            reason: "invalid authorization code",
            debug_details: Exception.message(e)
          )
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, %{"grant_type" => "authorization_code"}) do
    AsteroidWeb.Error.respond_api(
      conn,
      OAuth2.Request.InvalidRequestError.exception(reason: "Missing a mandatory parameter")
    )
  end

  # device code

  def handle(
        conn,
        %{
          "grant_type" => "urn:ietf:params:oauth:grant-type:device_code",
          "device_code" => device_code_param
        }
      ) do
    with :ok <- OAuth2.DeviceAuthorization.rate_limited?(device_code_param),
         :ok <-
           Asteroid.OAuth2.grant_type_enabled?(:"urn:ietf:params:oauth:grant-type:device_code"),
         {:ok, client} <- OAuth2.Client.get_client(conn),
         :ok <-
           OAuth2.Client.grant_type_authorized?(
             client,
             "urn:ietf:params:oauth:grant-type:device_code"
           ),
         {:ok, device_code} <- DeviceCode.get(device_code_param),
         :ok <- device_code_granted_to_client?(device_code, client),
         :ok <- device_code_access_granted?(device_code),
         {:ok, subject} <- Subject.load_from_unique_attribute("sub", device_code.data["sjid"]) do
      DeviceCode.delete(device_code)

      client = Client.fetch_attributes(client, ["client_id"])
      subject = Subject.fetch_attributes(subject, ["sub"])

      requested_scopes = Scope.Set.new(device_code.data["requested_scopes"] || [])
      granted_scopes = Scope.Set.new(device_code.data["granted_scopes"] || [])

      ctx =
        %{}
        |> Map.put(:endpoint, :token)
        |> Map.put(:flow, :device_authorization)
        |> Map.put(:grant_type, :"urn:ietf:params:oauth:grant-type:device_code")
        |> Map.put(:granted_scopes, granted_scopes)
        |> Map.put(:subject, subject)
        |> Map.put(:client, client)
        |> Map.put(:conn, conn)

      maybe_refresh_token =
        if astrenv(:oauth2_issue_refresh_token_callback).(ctx) do
          # FIXME: handle {:error, reason} failure case?
          {:ok, refresh_token} =
            RefreshToken.gen_new()
            |> RefreshToken.put_value("iat", now())
            |> RefreshToken.put_value(
              "exp",
              now() + astrenv(:oauth2_refresh_token_lifetime_callback).(ctx)
            )
            |> RefreshToken.put_value("client_id", client.attrs["client_id"])
            |> RefreshToken.put_value("sub", subject.attrs["sub"])
            |> RefreshToken.put_value("scope", Scope.Set.to_list(granted_scopes))
            |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "device_authorization")
            |> RefreshToken.put_value("iss", OAuth2.issuer())
            |> RefreshToken.store(ctx)

          refresh_token
        else
          nil
        end

      {:ok, access_token} =
        if maybe_refresh_token do
          new_access_token(ctx, refresh_token: maybe_refresh_token.id)
        else
          new_access_token(ctx)
        end
        |> AccessToken.put_value("iat", now())
        |> AccessToken.put_value(
          "exp",
          now() + astrenv(:oauth2_access_token_lifetime_callback).(ctx)
        )
        |> AccessToken.put_value("client_id", client.attrs["client_id"])
        |> AccessToken.put_value("sub", subject.attrs["sub"])
        |> AccessToken.put_value("scope", Scope.Set.to_list(granted_scopes))
        |> AccessToken.put_value("iss", OAuth2.issuer())
        |> AccessToken.store(ctx)

      resp =
        %{
          "access_token" => AccessToken.serialize(access_token),
          "expires_in" => access_token.data["exp"] - now(),
          "token_type" => "bearer"
        }
        |> maybe_put_refresh_token(maybe_refresh_token)
        |> put_scope_if_changed(requested_scopes, granted_scopes)
        |> astrenv(:oauth2_endpoint_token_grant_type_device_code_before_send_resp_callback).(ctx)

      conn
      |> put_status(200)
      |> put_resp_header("cache-control", "no-store")
      |> put_resp_header("pragma", "no-cache")
      |> astrenv(:oauth2_endpoint_token_grant_type_device_code_before_send_conn_callback).(ctx)
      |> json(resp)
    else
      {:error, %Token.InvalidTokenError{reason: "expired code"}} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.DeviceAuthorization.ExpiredTokenError.exception([])
        )

      {:error, %Token.InvalidTokenError{} = e} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.InvalidGrantError.exception(
            grant: "device code",
            reason: "invalid device code",
            debug_details: Exception.message(e)
          )
        )

      {:error, %AttributeRepository.Read.NotFoundError{} = e} ->
        AsteroidWeb.Error.respond_api(
          conn,
          OAuth2.ServerError.exception(
            reason: "could not read object in attribute repository: #{Exception.message(e)}"
          )
        )

      {:error, e} ->
        AsteroidWeb.Error.respond_api(conn, e)
    end
  end

  def handle(conn, %{"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"}) do
    AsteroidWeb.Error.respond_api(
      conn,
      OAuth2.Request.InvalidRequestError.exception(reason: "Missing `device_code` parameter")
    )
  end

  def handle(conn, %{"grant_type" => grant_type}) do
    AsteroidWeb.Error.respond_api(
      conn,
      OAuth2.UnsupportedGrantTypeError.exception(grant_type: grant_type)
    )
  end

  def handle(conn, _params) do
    AsteroidWeb.Error.respond_api(
      conn,
      OAuth2.Request.InvalidRequestError.exception(reason: "Missing `grant_type` parameter")
    )
  end

  @spec get_scope(String.t() | nil) :: {:ok, Scope.Set.t()} | {:error, Exception.t()}

  def get_scope(nil), do: {:ok, Scope.Set.new()}

  def get_scope(scope_param) do
    if Scope.oauth2_scope_param?(scope_param) do
      {:ok, Scope.Set.from_scope_param!(scope_param)}
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(
         name: "scope",
         value: scope_param
       )}
    end
  end

  @spec refresh_token_granted_to_client?(RefreshToken.t(), Client.t()) ::
          :ok
          | {:error, %OAuth2.InvalidGrantError{}}

  def refresh_token_granted_to_client?(refresh_token, client) do
    if refresh_token.data["client_id"] == client.id do
      :ok
    else
      {:error,
       OAuth2.InvalidGrantError.exception(
         grant: "authorization_code",
         reason: "invalid authorization code",
         debug_details: "request and authorization code client ids do not match"
       )}
    end
  end

  @spec device_code_granted_to_client?(DeviceCode.t(), Client.t()) ::
          :ok
          | {:error, %OAuth2.InvalidGrantError{}}

  def device_code_granted_to_client?(device_code, client) do
    if device_code.data["clid"] == client.id do
      :ok
    else
      {:error,
       OAuth2.InvalidGrantError.exception(
         grant: "device_code",
         reason: "invalid device code",
         debug_details: "device code does not match client id of the request"
       )}
    end
  end

  @spec device_code_access_granted?(DeviceCode.t()) :: :ok | {:error, Exception.t()}

  defp device_code_access_granted?(device_code) do
    case device_code.data["status"] do
      "granted" ->
        :ok

      "authorization_pending" ->
        {:error, OAuth2.DeviceAuthorization.AuthorizationPendingError.exception([])}

      "denied" ->
        {:error, OAuth2.AccessDeniedError.exception(reason: "access denied by the user")}
    end
  end

  @spec authorization_code_granted_to_client?(AuthorizationCode.t(), Client.t()) ::
          :ok
          | {:error, %OAuth2.InvalidGrantError{}}

  def authorization_code_granted_to_client?(authz_code, client) do
    if authz_code.data["client_id"] == client.id do
      :ok
    else
      {:error,
       OAuth2.InvalidGrantError.exception(
         grant: "authorization_code",
         reason: "invalid authorization code",
         debug_details: "request and authorization code client ids do not match"
       )}
    end
  end

  @spec redirect_uris_match?(AuthorizationCode.t(), OAuth2.RedirectUri.t()) ::
          :ok
          | {:error, %OAuth2.InvalidGrantError{}}

  defp redirect_uris_match?(authz_code, redirect_uri) do
    if authz_code.data["redirect_uri"] == redirect_uri do
      :ok
    else
      {:error,
       OAuth2.InvalidGrantError.exception(
         grant: "authorization_code",
         reason: "invalid authorization code",
         debug_details: "request and authorization code redirect uris do not match"
       )}
    end
  end

  @spec maybe_put_refresh_token(map(), RefreshToken.t()) :: map()

  defp maybe_put_refresh_token(map, %RefreshToken{} = refresh_token) do
    Map.put(map, "refresh_token", RefreshToken.serialize(refresh_token))
  end

  defp maybe_put_refresh_token(map, nil) do
    map
  end

  @spec put_scope_if_changed(map(), Scope.Set.t(), Scope.Set.t()) :: map()

  defp put_scope_if_changed(m, requested_scopes, granted_scopes) do
    if Scope.Set.equal?(requested_scopes, granted_scopes) do
      m
    else
      Map.put(m, "scope", Enum.join(granted_scopes, " "))
    end
  end

  @spec valid_username_param?(String.t()) :: :ok | {:error, %OAuth2.Request.MalformedParamError{}}

  defp valid_username_param?(username) do
    if OAuth2Utils.valid_username_param?(username) do
      :ok
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(
         name: "username",
         value: username
       )}
    end
  end

  @spec valid_password_param?(String.t()) :: :ok | {:error, %OAuth2.Request.MalformedParamError{}}

  defp valid_password_param?(password) do
    if OAuth2Utils.valid_password_param?(password) do
      :ok
    else
      {:error, OAuth2.Request.MalformedParamError.exception(name: "password", value: "[HIDDEN]")}
    end
  end

  @spec valid_refresh_token_param?(String.t()) ::
          :ok
          | {:error, %OAuth2.Request.MalformedParamError{}}

  defp valid_refresh_token_param?(refresh_token) do
    if OAuth2Utils.valid_refresh_token_param?(refresh_token) do
      :ok
    else
      {:error,
       OAuth2.Request.MalformedParamError.exception(
         name: "refresh_token",
         value: "[HIDDEN]"
       )}
    end
  end

  @spec pkce_code_verifier_valid?(AuthorizationCode.t(), OAuth2.PKCE.code_verifier() | nil) ::
          :ok
          | {:error, Exception.t()}

  defp pkce_code_verifier_valid?(authorization_code, code_verifier) do
    case {authorization_code.data["__asteroid_oauth2_pkce_code_challenge"], code_verifier} do
      {nil, _} ->
        :ok

      {_, nil} ->
        {:error,
         OAuth2.Request.InvalidRequestError.exception(reason: "Missing PKCE code verifier")}

      {code_challenge, code_verifier} ->
        code_challenge_method =
          authorization_code.data["__asteroid_oauth2_pkce_code_challenge_method"]
          |> OAuth2.PKCE.code_challenge_method_from_string()

        OAuth2.PKCE.verify_code_verifier(code_verifier, code_challenge, code_challenge_method)
    end
  end

  @spec new_access_token(Context.t(), Keyword.t()) :: AccessToken.t()

  defp new_access_token(ctx, access_token_opts \\ []) do
    serialization_format = astrenv(:oauth2_access_token_serialization_format_callback).(ctx)

    case serialization_format do
      :opaque ->
        AccessToken.gen_new(access_token_opts)

      :jws ->
        signing_key = astrenv(:oauth2_access_token_signing_key_callback).(ctx)
        signing_alg = astrenv(:oauth2_access_token_signing_alg_callback).(ctx)

        access_token_opts =
          access_token_opts
          |> Keyword.put(:serialization_format, serialization_format)
          |> Keyword.put(:signing_key, signing_key)
          |> Keyword.put(:signing_alg, signing_alg)

        AccessToken.gen_new(access_token_opts)
    end
  end
end


<|start_filename|>lib/asteroid/oidc.ex<|end_filename|>
defmodule Asteroid.OIDC do
  @moduledoc """
  Types and helper functions for OpenID Connect
  """

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.OAuth2
  alias Asteroid.Subject

  defmodule InteractionRequiredError do
    @moduledoc """
    Error returned when an interaction is required

    This error can happen when using the parameter `"prompt"` with the value `"none"`, if user
    interaction is required to retrieve new tokens
    """

    defexception []

    @type t :: %__MODULE__{}

    @impl true

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "interaction required"

        :normal ->
          "interaction required"

        :minimal ->
          ""
      end
    end
  end

  defmodule LoginRequiredError do
    @moduledoc """
    Error returned when a loging interaction is required

    This error can happen when using the parameter `"prompt"` with the value `"none"`,
    if new loging is required to retrieve tokens.
    """

    defexception []

    @type t :: %__MODULE__{}

    @impl true

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "login required"

        :normal ->
          "login required"

        :minimal ->
          ""
      end
    end
  end

  defmodule AccountSelectionRequiredError do
    @moduledoc """
    Error returned when account selection is required

    This error can happen when using the parameter `"prompt"` with the value `"none"`, if user
    account selection is required to retrieve new tokens
    """

    defexception []

    @type t :: %__MODULE__{}

    @impl true

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "account selection required"

        :normal ->
          "account selection required"

        :minimal ->
          ""
      end
    end
  end

  defmodule ConsentRequiredError do
    @moduledoc """
    Error returned when user consent is required

    This error can happen when using the parameter `"prompt"` with the value `"none"`, if user
    consent is required to retrieve new tokens.
    """

    defexception []

    @type t :: %__MODULE__{}

    @impl true

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "account selection required"

        :normal ->
          "account selection required"

        :minimal ->
          ""
      end
    end
  end

  @typedoc """
  Atoms describing the endpoints

  The values refer to:
  - `:userinfo`: `/api/oidc/userinfo`
  """

  @type endpoint :: :userinfo

  @typedoc """
  Nonce
  """

  @type nonce :: String.t()

  @typedoc """
  Claim name, such as `"phone_number"`
  """

  @type claim_name :: String.t()

  @typedoc """
  Authentication Class Reference
  """

  @type acr :: String.t()

  @typedoc """
  Authentication Method Reference
  """

  @type amr :: String.t()

  @doc """
  Returns `true` if OpenID Connect is enabled, `false` otherwise

  OpenID Connect is considered enabled if the `"openid"` scope is configured for one of the
  OpenID Connect flows (authorization code, implicit, or hybrid). For instance, one can
  activate OpenID Connect setting this scope on the root configuration scope:

  ```elixir
  config :asteroid, :scope_config, [scopes: %{"openid" => []}]
  ```
  """

  @spec enabled?() :: boolean()

  def enabled?() do
    "openid" in OAuth2.Scope.scopes_for_flow(:oidc_authorization_code) or
      "openid" in OAuth2.Scope.scopes_for_flow(:oidc_implicit) or
      "openid" in OAuth2.Scope.scopes_for_flow(:oidc_hybrid)
  end

  @doc """
  Returns the subject identifier, taking into account the subject type

  If the subject type is `"pairwise"`, it returns the hash of the concatenation of:
  - the hashed sector identifier's host component, or if missing the hasshed host component of
  the unique registered redirect URI for this client
  - the hashed subject id (and not the `"sub"`)
  - the salt configured by the `:oidc_subject_identifier_pairwise_salt` configuration option

  It uses sha256 as the hash function.
  """

  @spec subject_identifier(Subject.t(), Client.t()) :: String.t()

  def subject_identifier(subject, client) do
    subject = Subject.fetch_attributes(subject, ["sub"])

    client =
      Client.fetch_attributes(client, ["subject_type", "sector_identifier_uri", "redirect_uris"])

    if client.attrs["subject_type"] == "pairwise" do
      host_component =
        if client.attrs["sector_identifier_uri"] do
          URI.parse(client.attrs["sector_identifier_uri"]).host
        else
          # in this case all the redirect URIs should have the same host per the
          # OIDC dynamic client registration specification
          [redirect_uri | _] = client.attrs["redirect_uris"]

          URI.parse(redirect_uri).host
        end

      salt = astrenv(:oidc_subject_identifier_pairwise_salt)

      # we don't follow the OIDC specification here because hashing
      # sha256(a <> b) can result in same hash by carefully choosing a and b,
      # for instance the following pairs will have the same hash:
      # - a = "www.example.com", b = "alcom"
      # - a = "www.example.co", b = "malcom"

      hashed_subject_id = :crypto.hash(:sha256, subject.id)
      hashed_host_component = :crypto.hash(:sha256, host_component)

      :crypto.hash(:sha256, hashed_host_component <> hashed_subject_id <> salt)
      |> Base.url_encode64(padding: false)
    else
      subject.attrs["sub"]
    end
  end
end


<|start_filename|>lib/asteroid/oauth2/request.ex<|end_filename|>
defmodule Asteroid.OAuth2.Request do
  import Asteroid.Utils

  defmodule InvalidRequestError do
    @moduledoc """
    Error returned when an OAuth2 request is invalid

    Parameter is used internally to differentiate errors
    """

    @enforce_keys [:reason]

    defexception [:reason, :parameter]

    @type t :: %__MODULE__{
            reason: String.t(),
            parameter: String.t()
          }

    @impl true

    def message(%{reason: reason, parameter: parameter}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "invalid parameter `#{parameter}`, reason: #{reason}"

        :normal ->
          "invalid parameter `#{parameter}`"

        :minimal ->
          ""
      end
    end
  end

  defmodule MalformedParamError do
    @moduledoc """
    Error raised when an OAuth2 request param is malformed
    """

    defexception [:name, :value]

    @type t :: %__MODULE__{
            name: String.t(),
            value: String.t()
          }

    @impl true

    def message(%{name: name, value: value}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Malformed parameter `#{name}` with value `#{value}`"

        :normal ->
          "Malformed parameter `#{name}`"

        :minimal ->
          ""
      end
    end
  end
end


<|start_filename|>lib/asteroid_web/views/authorize_view.ex<|end_filename|>
defmodule AsteroidWeb.AuthorizeView do
  @moduledoc false

  use AsteroidWeb, :view
end


<|start_filename|>lib/asteroid/object_store/authentication_event/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticationEvent.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.AuthenticationEvent` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.AuthenticationEvent.opts()`) are:
  - `:table_name`: an `atom()` for the table name. Defaults to `:asteroid_authentication_event`
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased. One can use it to add additional indexes for clients or devices, e.g.:
  `tab_def: [index: :refresh_token, :authenticated_session_id, :client_id]`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `60` (1 minutes)

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :authenticated_session_id, :data],
    index: [:authenticated_session_id]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  alias Asteroid.OIDC.AuthenticationEvent

  @behaviour Asteroid.ObjectStore.AuthenticationEvent

  @impl true

  def install(opts) do
    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    table_name = opts[:table_name] || :asteroid_authentication_event

    tab_def =
      [
        attributes: [:id, :authenticated_session_id, :data],
        index: [:authenticated_session_id]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(table_name, tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created authentication event store #{table_name}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: authentication event store #{table_name} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create authentication event store #{table_name} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 60], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(authentication_event_id, opts) do
    table_name = opts[:table_name] || :asteroid_authentication_event

    case :mnesia.dirty_read(table_name, authentication_event_id) do
      [] ->
        Logger.debug(
          "#{__MODULE__}: getting authentication event `#{authentication_event_id}`, " <>
            "value: `nil`"
        )

        {:ok, nil}

      [{^table_name, ^authentication_event_id, authenticated_session_id, data}] ->
        authentication_event = %AuthenticationEvent{
          id: authentication_event_id,
          authenticated_session_id: authenticated_session_id,
          data: data
        }

        Logger.debug(
          "#{__MODULE__}: getting authentication event `#{authentication_event_id}`, " <>
            "value: `#{inspect(authentication_event)}`"
        )

        {:ok, authentication_event}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_authenticated_session_id(authenticated_session_id, opts) do
    table_name = opts[:table_name] || :asteroid_authentication_event

    {:ok,
     for {_table_name, authentication_event_id, _authenticated_session_id, _data} <-
           :mnesia.dirty_match_object({table_name, :_, authenticated_session_id, :_}) do
       authentication_event_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(authentication_event, opts) do
    table_name = opts[:table_name] || :asteroid_authentication_event

    record = {
      table_name,
      authentication_event.id,
      authentication_event.authenticated_session_id,
      authentication_event.data
    }

    :mnesia.dirty_write(table_name, record)

    Logger.debug(
      "#{__MODULE__}: stored authentication event `#{authentication_event.id}`, " <>
        "value: `#{inspect(authentication_event)}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(authentication_event_id, opts) do
    table_name = opts[:table_name] || :asteroid_authentication_event

    :mnesia.dirty_delete(table_name, authentication_event_id)

    Logger.debug("#{__MODULE__}: deleted authentication event `#{authentication_event_id}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>lib/asteroid/device.ex<|end_filename|>
defmodule Asteroid.Device do
  use AttributeRepository.Resource, otp_app: :asteroid

  @moduledoc """
  `AttributeRepository.Resource` for devices

  A device refers to a unique machine, be it a server, a smartphone, a personal computer, an
  IOT device such as a connected toothbrush...

  ## Configuration

  This modules uses the default configuration of `AttributeRepository.Resource` (see `config/1`).
  """
end


<|start_filename|>lib/asteroid/token/access_token.ex<|end_filename|>
defmodule Asteroid.Token.AccessToken do
  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.Token

  @moduledoc """
  Access token structure

  ## Field naming
  The `data` field holds the token data. The following field names are standard and are used
  by Asteroid:
  - `"exp"`: the expiration unix timestamp of the access token
  - `"sub"`: the `t:Asteroid.Subject.id()` of the access token
  - `"client_id"`: the `t:Asteroid.Client.id()` of the access token
  - `"scope"`: a list of `OAuth2Utils.Scope.scope()` scopes granted to the refresh token
  - `"device_id"`: the `t:Asteroid.Device.id()` of the access token
  - `"status"`: a `String.t()` for the status of the token. A token that has been revoked is not
  necessarily still present in the token store (e.g. for stateful tokens it will be probably
  deleted). Optionally one of:
    - `"active"`: active token
    - `"revoked"`: revoked token
  - `"__asteroid_oauth2_initial_flow"`: the initial `t:Asteroid.OAuth2.flow_str/0` that led to
  the issuance of this token
  - `"__asteroid_oidc_authenticated_session_id"`: the `t:Asteroid.OIDC.AuthenticatedSession.id/0`
  , if any
  - `"__asteroid_oidc_claims"`: the claims that were requested, if any
  """

  @enforce_keys [:id, :serialization_format, :data]

  defstruct [:id, :refresh_token_id, :serialization_format, :signing_key, :signing_alg, :data]

  @type id :: binary()

  @type t :: %__MODULE__{
          id: __MODULE__.id(),
          refresh_token_id: binary() | nil,
          serialization_format: Asteroid.Token.serialization_format(),
          signing_key: Asteroid.Crypto.Key.name() | nil,
          signing_alg: Asteroid.Crypto.Key.jws_alg() | nil,
          data: map()
        }

  @doc ~s"""
  Creates a new access token

  ## Options
  - `:id`: `String.t()` id, **mandatory**
  - `:refresh_token_id`: the `t:Asteroid.Token.RefreshToken.id/0` of the refresh token associated
  to this access token if any. Defaults to `nil`
  - `:data`: a data `map()`
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  - `:signing_key`: an `Asteroid.Crypto.Key.name()` for the signing key
  """

  @spec new(Keyword.t()) :: t()

  def new(opts) do
    %__MODULE__{
      id: opts[:id] || raise("Missing access token id"),
      refresh_token_id: opts[:refresh_token_id] || nil,
      data: opts[:data] || %{},
      serialization_format: opts[:serialization_format] || :opaque,
      signing_key: opts[:signing_key]
    }
  end

  @doc """
  Generates a new access token

  ## Options
  - `:refresh_token_id`: the `t:Asteroid.Token.RefreshToken.id/0` of the refresh token associated
  to this access token if any. Defaults to `nil`
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  - `:signing_key`: an `Asteroid.Crypto.Key.name()` for the signing key
  """

  @spec gen_new(Keyword.t()) :: t()
  def gen_new(opts \\ []) do
    %__MODULE__{
      id: secure_random_b64(20),
      refresh_token_id: opts[:refresh_token],
      data: %{},
      serialization_format:
        if(opts[:serialization_format], do: opts[:serialization_format], else: :opaque),
      signing_key: opts[:signing_key],
      signing_alg: opts[:signing_alg]
    }
  end

  @doc """
  Gets a access token from the access token store

  Unlike the `c:Asteroid.ObjectStore.AccessToken.get/2`, this function returns
  `{:error, Exception.t()}` if the access token is not found in the token
  store.

  ## Options
  - `:check_active`: determines whether the validity of the access token should be checked.
  Defaults to `true`. For validity checking details, see `active?/1`
  """

  @spec get(id(), Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}

  def get(access_token_id, opts \\ [check_active: true]) do
    at_store_module = astrenv(:object_store_access_token)[:module]
    at_store_opts = astrenv(:object_store_access_token)[:opts] || []

    case at_store_module.get(access_token_id, at_store_opts) do
      {:ok, access_token} when not is_nil(access_token) ->
        if opts[:check_active] != true or active?(access_token) do
          {:ok, access_token}
        else
          {:error,
           Token.InvalidTokenError.exception(
             sort: "access token",
             reason: "inactive token",
             id: access_token_id
           )}
        end

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "access token",
           reason: "not found in the token store",
           id: access_token_id
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Stores an access token

  This function only stores access tokens that have an `:opaque` serialization format.
  """

  @spec store(t(), Context.t()) :: {:ok, t()} | {:error, any()}

  def store(access_token, ctx \\ %{})

  def store(%__MODULE__{serialization_format: :opaque} = access_token, ctx) do
    at_store_module = astrenv(:object_store_access_token)[:module]
    at_store_opts = astrenv(:object_store_access_token)[:opts] || []

    access_token = astrenv(:object_store_access_token_before_store_callback).(access_token, ctx)

    case at_store_module.put(access_token, at_store_opts) do
      :ok ->
        {:ok, access_token}

      {:error, _} = error ->
        error
    end
  end

  def store(access_token, _ctx) do
    {:ok, access_token}
  end

  @doc """
  Deletes an access token
  """

  @spec delete(t() | id()) :: :ok | {:error, any()}

  def delete(%__MODULE__{id: id}) do
    delete(id)
  end

  def delete(access_token_id) do
    at_store_module = astrenv(:object_store_access_token)[:module]
    at_store_opts = astrenv(:object_store_access_token)[:opts] || []

    at_store_module.delete(access_token_id, at_store_opts)
  end

  @doc """
  Puts a value into the `data` field of access token

  If the value is `nil`, the access token is not changed and the filed is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(access_token, _key, nil), do: access_token

  def put_value(access_token, key, val) do
    %{access_token | data: Map.put(access_token.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of a access token

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(access_token, key) do
    %{access_token | data: Map.delete(access_token.data, key)}
  end

  @doc """
  Serializes the access token, using its inner `t:Asteroid.Token.serialization_format/0`
  information

  Supports serialization to `:opaque` and `:jws` serialization formats.

  In case of the serialization to the `jws` format:
  - if the signing algorithm was set, uses this algorithm
  - otherwise uses the default signer of `JOSE.JWT.sign/2`
  """

  @spec serialize(t()) :: String.t()

  def serialize(%__MODULE__{id: id, serialization_format: :opaque}) do
    id
  end

  def serialize(%__MODULE__{serialization_format: :jws} = access_token) do
    jwt =
      Enum.reduce(
        access_token.data,
        %{},
        fn
          {"__asteroid" <> _, _v}, acc ->
            acc

          {k, v}, acc ->
            Map.put(acc, k, v)
        end
      )

    {:ok, jwk} = Crypto.Key.get(access_token.signing_key)

    if access_token.signing_alg do
      jws = JOSE.JWS.from_map(%{"alg" => access_token.signing_alg})

      JOSE.JWT.sign(jwk, jws, jwt)
      |> JOSE.JWS.compact()
      |> elem(1)
    else
      JOSE.JWT.sign(jwk, jwt)
      |> JOSE.JWS.compact()
      |> elem(1)
    end
  end

  @doc """
  Returns `true` if the token is active, `false` otherwise

  The following data, *when set*, are used to determine that a token is active:
  - `"nbf"`: must be lower than current time
  - `"exp"`: must be higher than current time
  - `"revoked"`: must be the boolean `false`
  """

  @spec active?(t()) :: boolean()
  def active?(access_token) do
    (is_nil(access_token.data["nbf"]) or access_token.data["nbf"] < now()) and
      (is_nil(access_token.data["exp"]) or access_token.data["exp"] > now()) and
      (is_nil(access_token.data["status"]) or access_token.data["status"] != "revoked")

    # FIXME: implement the following items from https://tools.ietf.org/html/rfc7662#section-4
    #   o  If the token has been signed, the authorization server MUST
    #  validate the signature.
    #   o  If the token can be used only at certain resource servers, the
    #  authorization server MUST determine whether or not the token can
    #  be used at the resource server making the introspection call.
  end

  @doc """
  Returns the access token lifetime

  ## Processing rules
  - If the client has the following field set to an integer value for the corresponding flow
  returns that value:
    - `"__asteroid_oauth2_flow_ropc_access_token_lifetime"`
    - `"__asteroid_oauth2_flow_client_credentials_access_token_lifetime"`
    - `"__asteroid_oauth2_flow_authorization_code_access_token_lifetime"`
    - `"__asteroid_oauth2_flow_implicit_access_token_lifetime"`
    - `"__asteroid_oauth2_flow_device_authorization_access_token_lifetime"`
    - `"__asteroid_oidc_flow_authorization_code_access_token_lifetime"`
    - `"__asteroid_oidc_flow_implicit_access_token_lifetime"`
    - `"__asteroid_oidc_flow_hybrid_access_token_lifetime"`
  - Otherwise, if the following configuration option is set to an integer for the corresponding
  flow, returns its value:
    - #{Asteroid.Config.link_to_option(:oauth2_flow_ropc_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_client_credentials_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_implicit_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_device_authorization_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_implicit_access_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_access_token_lifetime)}
  - Otherwise returns
  #{Asteroid.Config.link_to_option(:oauth2_access_token_lifetime)}, or `0` if not set

  In any case, the returned value is capped by the scope configuration.
  """

  @spec lifetime(Context.t()) :: non_neg_integer()

  def lifetime(%{flow: flow, granted_scopes: granted_scopes} = ctx) do
    scope_config = Asteroid.OAuth2.Scope.configuration_for_flow(flow)

    case Asteroid.OAuth2.Scope.max_access_token_lifetime(granted_scopes, scope_config) do
      capped_lifetime when is_integer(capped_lifetime) ->
        min(lifetime_for_client(ctx), capped_lifetime)

      nil ->
        lifetime_for_client(ctx)
    end
  end

  # no scopes
  def lifetime(ctx) do
    lifetime_for_client(ctx)
  end

  @spec lifetime_for_client(Context.t()) :: non_neg_integer()

  def lifetime_for_client(%{flow: flow, client: client}) do
    attr =
      case flow do
        :ropc ->
          "__asteroid_oauth2_flow_ropc_access_token_lifetime"

        :client_credentials ->
          "__asteroid_oauth2_flow_client_credentials_access_token_lifetime"

        :authorization_code ->
          "__asteroid_oauth2_flow_authorization_code_access_token_lifetime"

        :implicit ->
          "__asteroid_oauth2_flow_implicit_access_token_lifetime"

        :device_authorization ->
          "__asteroid_oauth2_flow_device_authorization_access_token_lifetime"

        :oidc_authorization_code ->
          "__asteroid_oidc_flow_authorization_code_access_token_lifetime"

        :oidc_implicit ->
          "__asteroid_oidc_flow_implicit_access_token_lifetime"

        :oidc_hybrid ->
          "__asteroid_oidc_flow_hybrid_access_token_lifetime"
      end

    client = Client.fetch_attributes(client, [attr])

    case client.attrs[attr] do
      lifetime when is_integer(lifetime) ->
        lifetime

      _ ->
        conf_opt =
          case flow do
            :ropc ->
              :oauth2_flow_ropc_access_token_lifetime

            :client_credentials ->
              :oauth2_flow_client_credentials_access_token_lifetime

            :authorization_code ->
              :oauth2_flow_authorization_code_access_token_lifetime

            :implicit ->
              :oauth2_flow_implicit_access_token_lifetime

            :device_authorization ->
              :oauth2_flow_device_authorization_access_token_lifetime

            :oidc_authorization_code ->
              :oidc_flow_authorization_code_access_token_lifetime

            :oidc_implicit ->
              :oidc_flow_implicit_access_token_lifetime

            :oidc_hybrid ->
              :oidc_flow_hybrid_access_token_lifetime
          end

        astrenv(conf_opt, astrenv(:oauth2_access_token_lifetime, 0))
    end
  end

  @doc """
  Returns the serialization format for an access token

  Formalisation format is necessarily `:opaque`, except for access tokens for which the
  following rules apply (<FLOW> is to be replace by a `t:Asteroid.OAuth2.flow_str()/0`):
  - if the `__asteroid_oauth2_flow_<FLOW>_access_token_serialization_format` is set, returns
  this value
  - otherwise, if the `:oauth2_flow_<FLOW>_access_token_serialization_format` is set, returns
  this value
  - otherwise, returns the value of the
  #{Asteroid.Config.link_to_option(:oauth2_access_token_serialization_format)} configuration
  option
  - otherwise, returns `:opaque`
  """

  @spec serialization_format(Context.t()) :: Asteroid.Token.serialization_format()

  def serialization_format(%{flow: flow, client: client}) do
    attr = "__asteroid_oauth2_flow_#{Atom.to_string(flow)}_access_token_serialization_format"

    case flow do
      :ropc ->
        "__asteroid_oauth2_flow_ropc_access_token_serialization_format"

      :client_credentials ->
        "__asteroid_oauth2_flow_client_credentials_access_token_serialization_format"

      :authorization_code ->
        "__asteroid_oauth2_flow_authorization_code_access_token_serialization_format"

      :implicit ->
        "__asteroid_oauth2_flow_implicit_access_token_serialization_format"

      :device_authorization ->
        "__asteroid_oauth2_flow_device_authorization_access_token_serialization_format"

      :oidc_authorization_code ->
        "__asteroid_oidc_flow_authorization_code_access_token_serialization_format"

      :oidc_implicit ->
        "__asteroid_oidc_flow_implicit_access_token_serialization_format"

      :oidc_hybrid ->
        "__asteroid_oidc_flow_hybrid_access_token_serialization_format"
    end

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == "jws" do
      :jws
    else
      conf_opt =
        case flow do
          :ropc ->
            :oauth2_flow_ropc_access_token_serialization_format

          :client_credentials ->
            :oauth2_flow_client_credentials_access_token_serialization_format

          :authorization_code ->
            :oauth2_flow_authorization_code_access_token_serialization_format

          :implicit ->
            :oauth2_flow_implicit_access_token_serialization_format

          :device_authorization ->
            :oauth2_flow_device_authorization_access_token_serialization_format

          :oidc_authorization_code ->
            :oidc_flow_authorization_code_access_token_serialization_format

          :oidc_implicit ->
            :oidc_flow_implicit_access_token_serialization_format

          :oidc_hybrid ->
            :oidc_flow_hybrid_access_token_serialization_format
        end

      astrenv(conf_opt, astrenv(:oauth2_access_token_serialization_format, :opaque))
    end
  end

  @doc """
  Returns the signing key name for an access token

  The following rules apply (<FLOW> is to be replace by a `t:Asteroid.OAuth2.flow_str()/0`):
  - if the `__asteroid_oauth2_flow_<FLOW>_access_token_signing_key` is set, returns
  this value
  - otherwise, if the `:oauth2_flow_<FLOW>_access_token_signing_key` is set, returns
  this value
  - otherwise, returns the value of the
  #{Asteroid.Config.link_to_option(:oauth2_access_token_signing_key)} configuration
  option
  - otherwise, returns `nil`
  """

  @spec signing_key(Context.t()) :: Asteroid.Crypto.Key.name()

  def signing_key(%{flow: flow, client: client}) do
    attr =
      case flow do
        :ropc ->
          "__asteroid_oauth2_flow_ropc_access_token_signing_key"

        :client_credentials ->
          "__asteroid_oauth2_flow_client_credentials_access_token_signing_key"

        :authorization_code ->
          "__asteroid_oauth2_flow_authorization_code_access_token_signing_key"

        :implicit ->
          "__asteroid_oauth2_flow_implicit_access_token_signing_key"

        :device_authorization ->
          "__asteroid_oauth2_flow_device_authorization_access_token_signing_key"

        :oidc_authorization_code ->
          "__asteroid_oidc_flow_authorization_code_access_token_signing_key"

        :oidc_implicit ->
          "__asteroid_oidc_flow_implicit_access_token_signing_key"

        :oidc_hybrid ->
          "__asteroid_oidc_flow_hybrid_access_token_signing_key"
      end

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] != nil do
      client.attrs[attr]
    else
      conf_opt =
        case flow do
          :ropc ->
            :oauth2_flow_ropc_access_token_signing_key

          :client_credentials ->
            :oauth2_flow_client_credentials_access_token_signing_key

          :authorization_code ->
            :oauth2_flow_authorization_code_access_token_signing_key

          :implicit ->
            :oauth2_flow_implicit_access_token_signing_key

          :device_authorization ->
            :oauth2_flow_device_authorization_access_token_signing_key

          :oidc_authorization_code ->
            :oidc_flow_authorization_code_access_token_signing_key

          :oidc_implicit ->
            :oidc_flow_implicit_access_token_signing_key

          :oidc_hybrid ->
            :oidc_flow_hybrid_access_token_signing_key
        end

      astrenv(conf_opt, astrenv(:oauth2_access_token_signing_key))
    end
  end

  @doc """
  Returns the signing algortihm for an access token

  The following rules apply (<FLOW> is to be replace by a `t:Asteroid.OAuth2.flow_str()/0`):
  - if the `__asteroid_oauth2_flow_<FLOW>_access_token_signing_alg` is set, returns
  this value
  - otherwise, if the `:oauth2_flow_<FLOW>_access_token_signing_alg` is set, returns
  this value
  - otherwise, returns the value of the
  #{Asteroid.Config.link_to_option(:oauth2_access_token_signing_alg)} configuration
  option
  - otherwise, returns `nil`
  """

  @spec signing_alg(Context.t()) :: Asteroid.Crypto.Key.jws_alg()

  def signing_alg(%{flow: flow, client: client}) do
    attr =
      case flow do
        :ropc ->
          "__asteroid_oauth2_flow_ropc_access_token_signing_alg"

        :client_credentials ->
          "__asteroid_oauth2_flow_client_credentials_access_token_signing_alg"

        :authorization_code ->
          "__asteroid_oauth2_flow_authorization_code_access_token_signing_alg"

        :implicit ->
          "__asteroid_oauth2_flow_implicit_access_token_signing_alg"

        :device_authorization ->
          "__asteroid_oauth2_flow_device_authorization_access_token_signing_alg"

        :oidc_authorization_code ->
          "__asteroid_oidc_flow_authorization_code_access_token_signing_alg"

        :oidc_implicit ->
          "__asteroid_oidc_flow_implicit_access_token_signing_alg"

        :oidc_hybrid ->
          "__asteroid_oidc_flow_hybrid_access_token_signing_alg"
      end

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] != nil do
      client.attrs[attr]
    else
      conf_opt =
        case flow do
          :ropc ->
            :oauth2_flow_ropc_access_token_signing_alg

          :client_credentials ->
            :oauth2_flow_client_credentials_access_token_signing_alg

          :authorization_code ->
            :oauth2_flow_authorization_code_access_token_signing_alg

          :implicit ->
            :oauth2_flow_implicit_access_token_signing_alg

          :device_authorization ->
            :oauth2_flow_device_authorization_access_token_signing_alg

          :oidc_authorization_code ->
            :oidc_flow_authorization_code_access_token_signing_alg

          :oidc_implicit ->
            :oidc_flow_implicit_access_token_signing_alg

          :oidc_hybrid ->
            :oidc_flow_hybrid_access_token_signing_alg
        end

      astrenv(conf_opt, astrenv(:oauth2_access_token_signing_alg))
    end
  end
end


<|start_filename|>lib/asteroid/token/refresh_token.ex<|end_filename|>
defmodule Asteroid.Token.RefreshToken do
  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.Client
  alias Asteroid.Token

  @moduledoc """
  Refresh token structure

  ## Field naming
  The `data` field holds the token data. The following field names are standard and are used
  by Asteroid:
  - `"exp"`: the expiration unix timestamp of the refresh token
  - `"sub"`: the `t:Asteroid.Subject.id/0` of the refresh token
  - `"client_id"`: the `t:Asteroid.Client.id/0` of the refresh token
  - `"device_id"`: the `t:Asteroid.Device.id/0` of the refresh token
  - `"scope"`: a list of `OAuth2Utils.Scope.scope()` scopes granted to the refresh token
  - `"__asteroid_oauth2_initial_flow"`: the initial `t:Asteroid.OAuth2.flow_str/0` during which
  the refresh token was granted
  - `"__asteroid_oidc_authenticated_session_id"`: the `t:Asteroid.OIDC.AuthenticatedSession.id/0`
  of the refresh token
  - `"__asteroid_oidc_claims"`: the claims that were requested, if any
  - `"__asteroid_oidc_initial_acr"`: the `t:Asteroid.OIDC.acr/0` of the refresh token, if
  any. This is the value got from the session when the token was first released
  - `"__asteroid_oidc_initial_amr"`: a list of `t:Asteroid.OIDC.acr/0` of the refresh token,
  if any. This is the value got from the session when the token was first released
  - `"__asteroid_oidc_initial_auth_time"`: a `non_neg_integer()` of the refresh token,
  if any. This is the value got from the session when the token was first released
  - `"status"`: a `String.t()` for the status of the token. A token that has been revoked is not
  necessarily still present in the token store (e.g. for stateful tokens it will be probably
  deleted). Optionally one of:
    - `"active"`: active token
    - `"revoked"`: revoked token
  """

  @enforce_keys [:id, :serialization_format, :data]

  defstruct [:id, :data, :serialization_format]

  @type id :: binary()

  @type t :: %__MODULE__{
          id: __MODULE__.id(),
          serialization_format: Asteroid.Token.serialization_format(),
          data: map()
        }

  @doc ~s"""
  Creates a new refresh token

  ## Options
  - `:id`: `String.t()` id, **mandatory**
  - `:data`: a data `map()`
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  """

  @spec new(Keyword.t()) :: t()

  def new(opts) do
    %__MODULE__{
      id: opts[:id] || raise("Missing refresh token id"),
      data: opts[:data] || %{},
      serialization_format: opts[:serialization_format] || :opaque
    }
  end

  @doc """
  Generates a new refresh token

  ## Options
  - `:serialization_format`: an `t:Asteroid.Token.serialization_format/0` atom, defaults to
  `:opaque`
  """

  @spec gen_new(Keyword.t()) :: t()

  def gen_new(opts \\ []) do
    %__MODULE__{
      id: secure_random_b64(),
      data: %{},
      serialization_format: if(opts[:format], do: opts[:format], else: :opaque)
    }
  end

  @doc """
  Gets a refresh token from the refresh token store

  Unlike the `c:Asteroid.ObjectStore.RefreshToken.get/2`, this function returns
  `{:error, :nonexistent_refresh_token}` if the refresh token is not found in the token
  store.

  ## Options
  - `:check_active`: determines whether the validity of the refresh token should be checked.
  Defaults to `true`. For validity checking details, see `active?/1`
  """

  @spec get(id(), Keyword.t()) :: {:ok, t()} | {:error, Exception.t()}

  def get(refresh_token_id, opts \\ [check_active: true]) do
    rt_store_module = astrenv(:object_store_refresh_token)[:module]
    rt_store_opts = astrenv(:object_store_refresh_token)[:opts] || []

    case rt_store_module.get(refresh_token_id, rt_store_opts) do
      {:ok, refresh_token} when not is_nil(refresh_token) ->
        if opts[:check_active] != true or active?(refresh_token) do
          {:ok, refresh_token}
        else
          {:error,
           Token.InvalidTokenError.exception(
             sort: "refresh token",
             reason: "inactive token",
             id: refresh_token_id
           )}
        end

      {:ok, nil} ->
        {:error,
         Token.InvalidTokenError.exception(
           sort: "refresh token",
           reason: "not found in the token store",
           id: refresh_token_id
         )}

      {:error, error} ->
        {:error, error}
    end
  end

  @doc """
  Stores a refresh token
  """

  @spec store(t(), Context.t()) :: {:ok, t()} | {:error, any()}

  def store(refresh_token, ctx \\ %{}) do
    rt_store_module = astrenv(:object_store_refresh_token)[:module]
    rt_store_opts = astrenv(:object_store_refresh_token)[:opts] || []

    refresh_token =
      astrenv(:object_store_refresh_token_before_store_callback).(refresh_token, ctx)

    case rt_store_module.put(refresh_token, rt_store_opts) do
      :ok ->
        {:ok, refresh_token}

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Deletes a refresh token
  """

  @spec delete(t() | id()) :: :ok | {:error, any()}

  def delete(%__MODULE__{id: id}) do
    delete(id)
  end

  def delete(refresh_token_id) do
    rt_store_module = astrenv(:object_store_refresh_token)[:module]
    rt_store_opts = astrenv(:object_store_refresh_token)[:opts] || []

    rt_store_module.delete(refresh_token_id, rt_store_opts)

    at_store_module = astrenv(:object_store_access_token)[:module]
    at_store_opts = astrenv(:object_store_access_token)[:opts] || []

    case at_store_module.get_from_refresh_token_id(refresh_token_id, rt_store_opts) do
      {:ok, access_token_ids} ->
        for access_token_id <- access_token_ids do
          at_store_module.delete(access_token_id, at_store_opts)
        end

        :ok

      {:error, _} = error ->
        error
    end
  end

  @doc """
  Puts a value into the `data` field of refresh token

  If the value is `nil`, the refresh token is not changed and the filed is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(refresh_token, _key, nil), do: refresh_token

  def put_value(refresh_token, key, val) do
    %{refresh_token | data: Map.put(refresh_token.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of a refresh token

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(refresh_token, key) do
    %{refresh_token | data: Map.delete(refresh_token.data, key)}
  end

  @doc """
  Serializes the refresh token, using its inner `t:Asteroid.Token.serialization_format/0`
  information

  Supports serialization to `:opaque` serialization format.
  """

  @spec serialize(t()) :: String.t()

  def serialize(%__MODULE__{id: id, serialization_format: :opaque}) do
    id
  end

  @doc """
  Returns `true` if the token is active, `false` otherwise

  The following data, *when set*, are used to determine that a token is active:
  - `"nbf"`: must be lower than current time
  - `"exp"`: must be higher than current time
  - `"revoked"`: must be the boolean `false`
  """

  @spec active?(t()) :: boolean()

  def active?(refresh_token) do
    (is_nil(refresh_token.data["nbf"]) or refresh_token.data["nbf"] < now()) and
      (is_nil(refresh_token.data["exp"]) or refresh_token.data["exp"] > now()) and
      (is_nil(refresh_token.data["status"]) or refresh_token.data["status"] != "revoked")

    # FIXME: implement the following items from https://tools.ietf.org/html/rfc7662#section-4
    #   o  If the token has been signed, the authorization server MUST
    #  validate the signature.
    #   o  If the token can be used only at certain resource servers, the
    #  authorization server MUST determine whether or not the token can
    #  be used at the resource server making the introspection call.
  end

  @doc """
  Returns `true` if a refresh token is to be issued, `false` otherwise

  ## Processing rules
  - If the client has the following field set to `true` for the corresponding flow and
  grant type, returns `true`:
    - `"__asteroid_oauth2_flow_ropc_issue_refresh_token_init"`
    - `"__asteroid_oauth2_flow_ropc_issue_refresh_token_refresh"`
    - `"__asteroid_oauth2_flow_client_credentials_issue_refresh_token_init"`
    - `"__asteroid_oauth2_flow_client_credentials_issue_refresh_token_refresh"`
    - `"__asteroid_oauth2_flow_authorization_code_issue_refresh_token_init"`
    - `"__asteroid_oauth2_flow_authorization_code_issue_refresh_token_refresh"`
    - `"__asteroid_oauth2_flow_device_authorization_issue_refresh_token_init"`
    - `"__asteroid_oauth2_flow_device_authorization_issue_refresh_token_refresh"`
    - `"__asteroid_oidc_flow_authorization_code_issue_refresh_token_init"`
    - `"__asteroid_oidc_flow_authorization_code_issue_refresh_token_refresh"`
    - `"__asteroid_oidc_flow_hybrid_issue_refresh_token_init"`
    - `"__asteroid_oidc_flow_hybrid_issue_refresh_token_refresh"`
  - Otherwise, if the following configuration option is set to `true` for the corresponding flow
  and grant type, returns `true`:
    - #{Asteroid.Config.link_to_option(:oauth2_flow_ropc_issue_refresh_token_init)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_ropc_issue_refresh_token_refresh)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_client_credentials_issue_refresh_token_init)}
    - #{
    Asteroid.Config.link_to_option(:oauth2_flow_client_credentials_issue_refresh_token_refresh)
  }
    - #{Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_issue_refresh_token_init)}
    - #{
    Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_issue_refresh_token_refresh)
  }
    - #{
    Asteroid.Config.link_to_option(:oauth2_flow_device_authorization_issue_refresh_token_init)
  }
    - #{
    Asteroid.Config.link_to_option(:oauth2_flow_device_authorization_issue_refresh_token_refresh)
  }
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_issue_refresh_token_init)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_issue_refresh_token_refresh)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_issue_refresh_token_init)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_issue_refresh_token_refresh)}
  - Otherwise, uses the following configuration options:
    - #{Asteroid.Config.link_to_option(:oauth2_issue_refresh_token_init)}
    - #{Asteroid.Config.link_to_option(:oauth2_issue_refresh_token_refresh)}
  - Otherwise returns `false`
  """

  @spec issue_refresh_token?(Context.t()) :: boolean()

  def issue_refresh_token?(%{flow: :ropc, grant_type: :password, client: client}) do
    attr = "__asteroid_oauth2_flow_ropc_issue_refresh_token_init"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_ropc_issue_refresh_token_init,
        astrenv(:oauth2_issue_refresh_token_init, false)
      )
    end
  end

  def issue_refresh_token?(%{flow: :ropc, grant_type: :refresh_token, client: client}) do
    attr = "__asteroid_oauth2_flow_ropc_issue_refresh_token_refresh"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_ropc_issue_refresh_token_refresh,
        astrenv(:oauth2_issue_refresh_token_refresh, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :client_credentials,
        grant_type: :client_credentials,
        client: client
      }) do
    attr = "__asteroid_oauth2_flow_client_credentials_issue_refresh_token_init"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_client_credentials_issue_refresh_token_init,
        astrenv(:oauth2_issue_refresh_token_init, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :client_credentials,
        grant_type: :refresh_token,
        client: client
      }) do
    attr = "__asteroid_oauth2_flow_client_credentials_issue_refresh_token_refresh"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_client_credentials_issue_refresh_token_refresh,
        astrenv(:oauth2_issue_refresh_token_refresh, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :authorization_code,
        grant_type: :authorization_code,
        client: client
      }) do
    attr = "__asteroid_oauth2_flow_authorization_code_issue_refresh_token_init"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_authorization_code_issue_refresh_token_init,
        astrenv(:oauth2_issue_refresh_token_init, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :authorization_code,
        grant_type: :refresh_token,
        client: client
      }) do
    attr = "__asteroid_oauth2_flow_authorization_code_issue_refresh_token_refresh"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_authorization_code_issue_refresh_token_refresh,
        astrenv(:oauth2_issue_refresh_token_refresh, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :device_authorization,
        grant_type: :"urn:ietf:params:oauth:grant-type:device_code",
        client: client
      }) do
    attr = "__asteroid_oauth2_flow_device_authorization_issue_refresh_token_init"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_device_authorization_issue_refresh_token_init,
        astrenv(:oauth2_issue_refresh_token_init, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :device_authorization,
        grant_type: :refresh_token,
        client: client
      }) do
    attr = "__asteroid_oauth2_flow_device_authorization_issue_refresh_token_refresh"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oauth2_flow_device_authorization_issue_refresh_token_refresh,
        astrenv(:oauth2_issue_refresh_token_refresh, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :oidc_authorization_code,
        grant_type: :authorization_code,
        client: client
      }) do
    attr = "__asteroid_oidc_flow_authorization_code_issue_refresh_token_init"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oidc_flow_authorization_code_issue_refresh_token_init,
        astrenv(:oauth2_issue_refresh_token_init, false)
      )
    end
  end

  def issue_refresh_token?(%{
        flow: :oidc_authorization_code,
        grant_type: :refresh_token,
        client: client
      }) do
    attr = "__asteroid_oidc_flow_authorization_code_issue_refresh_token_refresh"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oidc_flow_authorization_code_issue_refresh_token_refresh,
        astrenv(:oauth2_issue_refresh_token_refresh, false)
      )
    end
  end

  def issue_refresh_token?(%{flow: :oidc_hybrid, grant_type: :authorization_code, client: client}) do
    attr = "__asteroid_oidc_flow_hybrid_issue_refresh_token_init"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oidc_flow_hybrid_issue_refresh_token_init,
        astrenv(:oauth2_issue_refresh_token_init, false)
      )
    end
  end

  def issue_refresh_token?(%{flow: :oidc_hybrid, grant_type: :refresh_token, client: client}) do
    attr = "__asteroid_oidc_flow_hybrid_issue_refresh_token_refresh"

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      astrenv(
        :oidc_flow_authorization_code_issue_refresh_token_refresh,
        astrenv(:oauth2_issue_refresh_token_refresh, false)
      )
    end
  end

  def issue_refresh_token?(_) do
    false
  end

  @doc """
  Returns the refresh token lifetime

  ## Processing rules
  - If the client has the following field set to an integer value for the corresponding flow
  returns that value:
    - `"__asteroid_oauth2_flow_ropc_refresh_token_lifetime"`
    - `"__asteroid_oauth2_flow_client_credentials_refresh_token_lifetime"`
    - `"__asteroid_oauth2_flow_authorization_code_refresh_token_lifetime"`
    - `"__asteroid_oauth2_flow_device_authorization_refresh_token_lifetime"`
    - `"__asteroid_oidc_flow_authorization_code_refresh_token_lifetime"`
    - `"__asteroid_oidc_flow_hybrid_refresh_token_lifetime"`
  - Otherwise, if the following configuration option is set to an integer for the corresponding
  flow, returns its value:
    - #{Asteroid.Config.link_to_option(:oauth2_flow_ropc_refresh_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_client_credentials_refresh_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_authorization_code_refresh_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oauth2_flow_device_authorization_refresh_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_refresh_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_refresh_token_lifetime)}
  - Otherwise returns the value of the
  #{Asteroid.Config.link_to_option(:oauth2_refresh_token_lifetime)} configuration option
  - Otherwise returns `0`

  In any case, the returned value is capped by the scope configuration.
  """

  @spec lifetime(Context.t()) :: non_neg_integer()

  def lifetime(%{flow: flow, granted_scopes: granted_scopes} = ctx) do
    scope_config = Asteroid.OAuth2.Scope.configuration_for_flow(flow)

    case Asteroid.OAuth2.Scope.max_refresh_token_lifetime(granted_scopes, scope_config) do
      capped_lifetime when is_integer(capped_lifetime) ->
        min(lifetime_for_client(ctx), capped_lifetime)

      nil ->
        lifetime_for_client(ctx)
    end
  end

  # no scopes
  def lifetime(ctx) do
    lifetime_for_client(ctx)
  end

  @spec lifetime_for_client(Context.t()) :: non_neg_integer()

  defp lifetime_for_client(%{flow: flow, client: client}) do
    attr =
      case flow do
        :ropc ->
          "__asteroid_oauth2_flow_ropc_refresh_token_lifetime"

        :client_credentials ->
          "__asteroid_oauth2_flow_client_credentials_refresh_token_lifetime"

        :authorization_code ->
          "__asteroid_oauth2_flow_authorization_code_refresh_token_lifetime"

        :device_authorization ->
          "__asteroid_oauth2_flow_device_authorization_refresh_token_lifetime"

        :oidc_authorization_code ->
          "__asteroid_oidc_flow_authorization_code_refresh_token_lifetime"

        :oidc_hybrid ->
          "__asteroid_oidc_flow_hybrid_refresh_token_lifetime"
      end

    client = Client.fetch_attributes(client, [attr])

    case client.attrs[attr] do
      lifetime when is_integer(lifetime) ->
        lifetime

      _ ->
        conf_opt =
          case flow do
            :ropc ->
              :oauth2_flow_ropc_refresh_token_lifetime

            :client_credentials ->
              :oauth2_flow_client_credentials_refresh_token_lifetime

            :authorization_code ->
              :oauth2_flow_authorization_code_refresh_token_lifetime

            :device_authorization ->
              :oauth2_flow_device_authorization_refresh_token_lifetime

            :oidc_authorization_code ->
              :oidc_flow_authorization_code_refresh_token_lifetime

            :oidc_hybrid ->
              :oidc_flow_hybrid_refresh_token_lifetime
          end

        astrenv(conf_opt, astrenv(:oauth2_refresh_token_lifetime, 0))
    end
  end

  defp lifetime_for_client(_) do
    0
  end
end


<|start_filename|>lib/asteroid/object_store/authentication_event.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AuthenticationEvent do
  @moduledoc """
  Behaviour for authentication event store
  """

  @type opts :: Keyword.t()

  @doc """
  Installs the authentication event store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the authentication event store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the authentication event store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an authentication event from its id

  Returns `{:ok, %Asteroid.OIDC.AuthenticationEvent{}}` if the authentication event exists
  and `{:ok, nil}` otherwise.
  """

  @callback get(Asteroid.OIDC.AuthenticationEvent.id(), opts()) ::
              {:ok, Asteroid.OIDC.AuthenticationEvent.t() | nil}
              | {:error, any()}

  @doc """
  Returns all the *authentication event ids* of a subject
  """

  @callback get_from_authenticated_session_id(Asteroid.Subject.id(), opts()) ::
              {:ok, [Asteroid.OIDC.AuthenticationEvent.id()]} | {:error, any()}

  @doc """
  Stores an authentication event

  If the authentication event already exists, all of its data should be erased.
  """

  @callback put(Asteroid.OIDC.AuthenticationEvent.t(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an authentication event
  """

  @callback delete(Asteroid.OIDC.AuthenticationEvent.id(), opts()) :: :ok | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1
end


<|start_filename|>test/support/callbacks.ex<|end_filename|>
defmodule Asteroid.Test.Callbacks do
  @moduledoc false

  alias Asteroid.Subject
  alias OAuth2Utils.Scope

  @doc false

  @spec test_ropc_username_password_callback(Plug.Conn.t(), String.t(), String.t()) ::
          {:ok, Asteroid.Subject.t()} | {:error, atom()}

  def test_ropc_username_password_callback(_conn, username, password) do
    case Subject.load_from_unique_attribute("sub", username, attributes: ["password"]) do
      {:ok, sub} ->
        if sub.attrs["password"] == password do
          {:ok, sub}
        else
          {:error,
           Asteroid.OAuth2.InvalidGrantError.exception(
             grant: "password",
             reason: "invalid username or password",
             debug_details: "passwords don't match"
           )}
        end

      {:error, reason} ->
        {:error, reason}
    end
  end

  @spec authorize_print_successful_request(
          Plug.Conn.t(),
          %AsteroidWeb.AuthorizeController.Request{}
        ) ::
          Plug.Conn.t()

  def authorize_print_successful_request(conn, request) do
    request_map =
      Map.from_struct(%{
        request
        | client_id: request.client_id,
          requested_scopes: Scope.Set.to_list(request.requested_scopes)
      })

    conn
    |> Plug.Conn.put_status(200)
    |> Phoenix.Controller.json(request_map)
  end
end


<|start_filename|>lib/asteroid/oauth2/register.ex<|end_filename|>
defmodule Asteroid.OAuth2.Register do
  @moduledoc """
  Utils fnuction related to client registration
  """

  import Asteroid.Utils

  alias Asteroid.Client
  alias Asteroid.OAuth2
  alias OAuth2Utils.Scope

  @doc """
  Returns `:ok` if the request is authorized to register new clients,
  `{:error, Exception.t()}` otherwise

  Unauthenticated requests are authorized only if the
  #{Asteroid.Config.link_to_option(:oauth2_endpoint_register_authorization_policy)} is set
  to `:all`.

  A client passed as a parameter shall be authenticated.

  When using the HTTP `Bearer` scheme, it is not required that the client exists in Asteroid.
  """

  @spec request_authorized?(Plug.Conn.t(), Client.t() | nil) ::
          :ok
          | {:error, %OAuth2.Client.AuthenticationError{}}
          | {:error, %OAuth2.Client.AuthorizationError{}}

  def request_authorized?(conn, maybe_authenticated_client) do
    case astrenv(:oauth2_endpoint_register_authorization_policy) do
      :all ->
        :ok

      :authenticated_clients ->
        if APIac.authenticated?(conn) do
          :ok
        else
          if maybe_authenticated_client do
            {:error,
             Asteroid.OAuth2.Client.AuthenticationError.exception(
               reason: :client_authentication_required
             )}
          else
            {:error,
             Asteroid.OAuth2.Client.AuthenticationError.exception(
               reason: :client_authentication_required
             )}
          end
        end

      :authorized_clients ->
        if APIac.metadata(conn)["scope"] != nil and
             "asteroid.register" in Scope.Set.from_scope_param!(APIac.metadata(conn)["scope"]) do
          :ok
        else
          if maybe_authenticated_client do
            client = Client.fetch_attributes(maybe_authenticated_client, ["client_id", "scope"])

            if "asteroid.register" in client.attrs["scope"] do
              :ok
            else
              {:error,
               Asteroid.OAuth2.Client.AuthorizationError.exception(reason: :unauthorized_client)}
            end
          else
            {:error,
             Asteroid.OAuth2.Client.AuthenticationError.exception(
               reason: :client_authentication_required
             )}
          end
        end
    end
  end

  @doc """
  Returns `:ok` if the grant types and the responses types are consistent,
  `{:error, String.t()}` otherwise.

  The error response's string contains a human-readable explanation of the reason it is not
  consistent.

  The following table is used for consistency check:


  | grant_types value includes:                   | response_types      |
  |-----------------------------------------------|---------------------|
  | authorization_code                            | code                |
  | implicit                                      | token               |
  | implicit                                      | id_token            |
  | implicit                                      | token id_token      |
  | authorization_code, implicit                  | code id_token       |
  | authorization_code, implicit                  | code token          |
  | authorization_code, implicit                  | code id_token token |
  | password                                      | (none)              |
  | client_credentials                            | (none)              |
  | refresh_token                                 | (none)              |
  | urn:ietf:params:oauth:grant-type:jwt-bearer   | (none)              |
  | urn:ietf:params:oauth:grant-type:saml2-bearer | (none)              |

  """

  @spec grant_response_types_consistent?(
          [OAuth2.grant_type_str()],
          [OAuth2.response_type_str()]
        ) ::
          :ok
          | {:error, String.t()}

  def grant_response_types_consistent?(grant_types, response_types) do
    mapping = %{
      "code" => ["authorization_code"],
      "token" => ["implicit"],
      "id_token" => ["implicit"],
      "token id_token" => ["implicit"],
      "code id_token" => ["authorization_code", "implicit"],
      "code token" => ["authorization_code", "implicit"],
      "code id_token token" => ["authorization_code", "implicit"]
    }

    Enum.reduce_while(
      response_types,
      :ok,
      fn
        response_type, _acc ->
          mandatory_grant_types = mapping[response_type]

          if Enum.all?(mandatory_grant_types, &(&1 in grant_types)) do
            {:cont, :ok}
          else
            {:halt,
             {:error, "response_type `#{response_type}` have missing mandatory grant type"}}
          end
      end
    )
  end

  @doc """
  Generates a new id for the client attribute repository

  This function simply returns the client id passed in the processed metadata
  """

  @spec generate_client_resource_id(processed_metadata :: map(), Asteroid.Context.t()) ::
          AttributeRepository.resource_id()

  def generate_client_resource_id(%{"client_id" => client_id}, _ctx), do: client_id

  @doc """
  Generates a new OAuth2 client id

  It takes into parameter the map of the parsed and controlled client metadata ready to be
  returned to the initiating client.

  This function generates a new `client_id` given the following rules:
  - If the map value of `"client_name"` is a string:
    - Strips the parameters from invalid characters (not in the range \x20-\x7E)
    - Lowercase this string
    - Replace spaces by underscores
    - If the resulting string is not the empty string:
      - Returns it if no client as already the same `client_id`
      - Otherwise, append `"_1"`, `"_2"`, `"_3"`, `"_4"`... to `"_99"` until there's no client
      with that `client_id` already in the base
      - If all these client exists, returns a 20 bytes random string
    - Otherwise returns a 20 bytes random string
  - Otherwise returns a 20 bytes random string
  """

  @spec generate_client_id(map(), Asteroid.Context.t()) :: String.t()

  def generate_client_id(%{"client_name" => client_name}, _ctx) when is_binary(client_name) do
    client_name_sanitized =
      client_name
      |> String.replace(~r/[^\x20-\x7E]/, "")
      |> String.downcase(:ascii)
      |> String.replace(" ", "_")

    gen_new_client_id_from_client_name(client_name_sanitized, 0)
  end

  def generate_client_id(_, _ctx) do
    secure_random_b64(20)
  end

  @spec gen_new_client_id_from_client_name(String.t(), non_neg_integer()) :: String.t()

  defp gen_new_client_id_from_client_name(_client_name, n) when n >= 100 do
    secure_random_b64(20)
  end

  # client is only composed of special chars - no interest to use client name here

  defp gen_new_client_id_from_client_name("", _) do
    secure_random_b64(20)
  end

  defp gen_new_client_id_from_client_name(client_name, n) do
    client_name_suffixed =
      case n do
        0 ->
          client_name

        _ ->
          client_name <> "_" <> Integer.to_string(n)
      end

    case Client.load_from_unique_attribute("client_id", client_name_suffixed, attributes: []) do
      {:error, %AttributeRepository.Read.NotFoundError{}} ->
        # the client doesn't exist
        client_name_suffixed

      {:ok, _} ->
        gen_new_client_id_from_client_name(client_name, n + 1)
    end
  end

  @doc """
  Returns the computed client's type

  Returns `:public` if the client `"token_endpoint_auth_method"` is set to `"none"`,
  `:confidential` otherwise.
  """

  @spec client_type(Client.t()) :: OAuth2.Client.type()

  def client_type(client) do
    client = Client.fetch_attributes(client, ["token_endpoint_auth_method"])

    if client.attrs["token_endpoint_auth_method"] == "none" do
      :public
    else
      :confidential
    end
  end
end


<|start_filename|>test/asteroid_web/controllers/authorize_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.AuthorizeControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias Asteroid.Subject
  alias OAuth2Utils.Scope

  ##########################
  # Invalid requests
  ##########################

  test "Missing response_type parameter with valid client_id & redirect_uri", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "Missing response_type and no client_id & redirect_uri", %{conn: conn} do
    params = %{}

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert html_response(conn, 400) =~ "missing parameter"
  end

  test "Missing redirect uri", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_1"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert html_response(conn, 400) =~ "missing parameter"
  end

  test "Malformed redirect uri", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_1",
      "redirect_uri" => "wxthwermgkzawu"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert html_response(conn, 400) =~ "Malformed parameter"
    assert html_response(conn, 400) =~ "redirect_uri"
  end

  test "Redirect uri not registered for client", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://example.br"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert html_response(conn, 400) =~ "unregistered"
    assert html_response(conn, 400) =~ "redirect_uri"
  end

  test "client_id is missing", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "redirect_uri" => "https://example.br"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert html_response(conn, 400) =~ "missing parameter"
  end

  test "client_id is malformed", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "invalid_cłiend_id",
      "redirect_uri" => "https://example.br"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert html_response(conn, 400) =~ "Malformed parameter"
    assert html_response(conn, 400) =~ "client_id"
  end

  test "response_type not enabled", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com"
    }

    Process.put(:oauth2_response_types_enabled, [])

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "unsupported_response_type"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "Unauthorized response_type for client", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_2",
      "redirect_uri" => "https://www.example.com"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "unauthorized_client"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "Malformed scope param", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "scope" => "scp1 s¢p2 scp3"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert %{"error" => "invalid_scope"} = URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "Unauthorized scope param", %{conn: conn} do
    params = %{
      "response_type" => "code",
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "scope" => "scp1 scp2 scp3 scp99"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"
    assert %{"error" => "invalid_scope"} = URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  ##########################
  # Authorization denied
  ##########################

  test "Authorization denied - access denied with no state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_denied(
        conn,
        %{
          authz_request: authz_request,
          error: OAuth2.AccessDeniedError.exception(reason: "abc def")
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "access_denied"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "abc def"

    refute URI.decode_query(URI.parse(redirected_to(conn)).query)["state"]
  end

  test "Authorization denied - access denied with state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_denied(
        conn,
        %{
          authz_request: authz_request,
          error: OAuth2.AccessDeniedError.exception(reason: "abc def")
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "access_denied"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "abc def"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["state"] ==
             "sxgjwzedrgdfchexgim"
  end

  test "Authorization denied - server error with no state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_denied(
        conn,
        %{
          authz_request: authz_request,
          error: OAuth2.ServerError.exception(reason: "abc def")
        }
      )

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] == "server_error"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "abc def"
  end

  test "Authorization denied - temporarily unavailable with no state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_denied(
        conn,
        %{
          authz_request: authz_request,
          error: OAuth2.TemporarilyUnavailableError.exception(reason: "abc def")
        }
      )

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error"] ==
             "temporarily_unavailable"

    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["error_description"] =~
             "abc def"
  end

  ##########################
  # Authorization granted (code)
  ##########################

  test "Authorization granted (code) - access granted with state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"code" => _, "state" => "sxgjwzedrgdfchexgim"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)

    %{"code" => az_code} = URI.decode_query(URI.parse(redirected_to(conn)).query)

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_confidential_1"
    assert authorization_code.data["sub"] == "user_1"
  end

  test "Authorization granted (code) - access granted without state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"
    assert %{"code" => az_code} = URI.decode_query(URI.parse(redirected_to(conn)).query)
    assert URI.decode_query(URI.parse(redirected_to(conn)).query)["state"] == nil

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_confidential_1"
    assert authorization_code.data["sub"] == "user_1"
  end

  ##########################
  # Authorization granted (implicit)
  ##########################

  test "Authorization granted (implicit) - access granted with state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :implicit,
      response_type: :token,
      response_mode: :fragment,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "access_token" => access_token,
             "token_type" => "bearer",
             "expires_in" => _,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token)

    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"
  end

  test "Authorization granted (implicit) - access granted without state", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :implicit,
      response_type: :token,
      response_mode: :fragment,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "access_token" => access_token,
             "token_type" => "bearer",
             "expires_in" => _
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    assert URI.decode_query(URI.parse(redirected_to(conn)).fragment)["state"] == nil

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token)

    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"
  end

  test "Authorization granted (implicit) - access granted with differing scopes", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :implicit,
      response_type: :token,
      response_mode: :fragment,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: Scope.Set.new(["scp1", "scp2", "scp3", "scp4"]),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(["scp2", "scp4", "scp3"])
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "access_token" => access_token,
             "token_type" => "bearer",
             "expires_in" => _,
             "scope" => granted_scopes,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(granted_scopes),
             Scope.Set.new(["scp2", "scp4", "scp3"])
           )

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token)

    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"
  end

  test "Authorization granted (implicit) - access granted with capped access token lifetime", %{
    conn: conn
  } do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :implicit,
      response_type: :token,
      response_mode: :fragment,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: Scope.Set.new(["scp1", "scp2", "scp3", "scp4"]),
      params: %{"state" => "sxgjwzedrgdfchexgim"}
    }

    Process.put(:oauth2_flow_implicit_scope_config,
      scopes: %{
        "scp1" => [],
        "scp2" => [],
        "scp3" => [],
        "scp4" => [max_refresh_token_lifetime: 1000, max_access_token_lifetime: 30]
      }
    )

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new(["scp2", "scp4", "scp3", "scp1"])
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "access_token" => access_token,
             "token_type" => "bearer",
             "expires_in" => expires_in,
             "state" => "sxgjwzedrgdfchexgim"
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    {:ok, access_token} = Asteroid.Token.AccessToken.get(access_token)

    assert expires_in |> Integer.parse() |> elem(0) >= 29
    assert expires_in |> Integer.parse() |> elem(0) <= 31
    assert access_token.data["client_id"] == "client_confidential_1"
    assert access_token.data["sub"] == "user_1"
    assert access_token.data["exp"] >= now() + 29
    assert access_token.data["exp"] <= now() + 31
  end

  test "Authorization granted (implicit) - access granted with JWS access token", %{conn: conn} do
    Process.put(:oauth2_flow_implicit_access_token_serialization_format, :jws)
    Process.put(:oauth2_flow_implicit_access_token_signing_key, "key_auto_sig")
    Process.put(:oauth2_flow_implicit_access_token_signing_alg, "RS384")

    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :implicit,
      response_type: :token,
      response_mode: :fragment,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{
             "access_token" => access_token,
             "token_type" => "bearer",
             "expires_in" => _
           } = URI.decode_query(URI.parse(redirected_to(conn)).fragment)

    assert URI.decode_query(URI.parse(redirected_to(conn)).fragment)["state"] == nil

    jws_at = URI.decode_query(URI.parse(redirected_to(conn)).fragment)["access_token"]

    {:ok, jwk} = Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, access_token_str, _} = JOSE.JWS.verify_strict(jwk, ["RS384"], jws_at)

    access_token_data = Jason.decode!(access_token_str)

    assert access_token_data["client_id"] == "client_confidential_1"
    assert access_token_data["sub"] == "user_1"
  end

  ##########################
  # PKCE
  ##########################

  test "PKCE - error: mandatory set globally but not used", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code"
    }

    Process.put(:oauth2_pkce_policy, :mandatory)

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - error: client doesn't use PKCE but must", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_3",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - error: code challenge too short", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "code_challenge" => "too_short"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - error: code challenge too long", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "code_challenge" => String.duplicate("x", 129)
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - error: code challenge contains an invalid character", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "code_challenge" => String.duplicate("x", 50) <> "ṽxxxxx"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - error: code challenge method is unsupported", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "code_challenge" => String.duplicate("x", 50),
      "code_challenge_method" => "unknown"
    }

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - error: code challenge method is not enabled", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "code_challenge" => String.duplicate("x", 50),
      "code_challenge_method" => "plain"
    }

    Process.put(:oauth2_pkce_allowed_methods, [:S256])

    conn = get(conn, "/authorize?#{URI.encode_query(params)}")

    assert redirected_to(conn) =~ "https://www.example.com"

    assert %{"error" => "invalid_request"} =
             URI.decode_query(URI.parse(redirected_to(conn)).query)
  end

  test "PKCE - success request", %{conn: conn} do
    params = %{
      "client_id" => "client_confidential_1",
      "redirect_uri" => "https://www.example.com",
      "response_type" => "code",
      "code_challenge" => String.duplicate("x", 50),
      "code_challenge_method" => "S256"
    }

    Process.put(
      :oauth2_flow_authorization_code_web_authorization_callback,
      fn conn, request ->
        request_map =
          Map.from_struct(%{
            request
            | client_id: request.client_id,
              requested_scopes: Scope.Set.to_list(request.requested_scopes)
          })

        conn
        |> put_status(200)
        |> Phoenix.Controller.json(request_map)
      end
    )

    response =
      conn
      |> get("/authorize?#{URI.encode_query(params)}")
      |> json_response(200)

    assert response["pkce_code_challenge"] == String.duplicate("x", 50)
    assert response["pkce_code_challenge_method"] == "S256"
  end

  test "PKCE (code) - access granted", %{conn: conn} do
    authz_request = %AsteroidWeb.AuthorizeController.Request{
      flow: :authorization_code,
      response_type: :code,
      response_mode: :query,
      client_id: "client_confidential_1",
      redirect_uri: "https://www.example.com",
      requested_scopes: MapSet.new(),
      pkce_code_challenge: String.duplicate("x", 50),
      pkce_code_challenge_method: :S256,
      params: %{}
    }

    conn =
      AsteroidWeb.AuthorizeController.authorization_granted(
        conn,
        %{
          authz_request: authz_request,
          subject: Subject.load("user_1") |> elem(1),
          granted_scopes: Scope.Set.new()
        }
      )

    assert redirected_to(conn) =~ "https://www.example.com"
    assert %{"code" => az_code} = URI.decode_query(URI.parse(redirected_to(conn)).query)

    {:ok, authorization_code} = Asteroid.Token.AuthorizationCode.get(az_code)

    assert authorization_code.data["client_id"] == "client_confidential_1"
    assert authorization_code.data["sub"] == "user_1"

    assert authorization_code.data["__asteroid_oauth2_pkce_code_challenge"] ==
             String.duplicate("x", 50)

    assert authorization_code.data["__asteroid_oauth2_pkce_code_challenge_method"] == "S256"
  end
end


<|start_filename|>lib/asteroid_web/controllers/well_known/oauth_authorization_server_controller.ex<|end_filename|>
defmodule AsteroidWeb.WellKnown.OauthAuthorizationServerController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias Asteroid.OAuth2
  alias Asteroid.OIDC
  alias AsteroidWeb.Router.Helpers, as: Routes

  def handle(conn, _params) do
    metadata =
      %{}
      |> Map.put("issuer", OAuth2.issuer())
      |> maybe_put_authorization_endpoint()
      |> maybe_put_token_endpoint()
      |> put_registration_endpoint()
      |> put_scopes_supported()
      |> put_response_types_supported()
      |> put_grant_types_supported()
      |> put_token_endpoint_auth_method_supported()
      |> put_jwks_uri()
      |> put_revocation_endpoint()
      |> put_revocation_endpoint_auth_method_supported()
      |> put_introspection_endpoint()
      |> put_introspection_endpoint_auth_method_supported()
      |> put_device_authorization_endpoint()
      |> put_code_challenge_methods_supported()
      |> put_if_not_nil(
        "service_documentation",
        astrenv(:oauth2_endpoint_metadata_service_documentation, nil)
      )
      |> put_if_not_nil(
        "ui_locales_supported",
        astrenv(:oauth2_endpoint_metadata_ui_locales_supported, nil)
      )
      |> put_if_not_nil(
        "op_policy_uri",
        astrenv(:oauth2_endpoint_metadata_op_policy_uri, nil)
      )
      |> put_if_not_nil(
        "op_tos_uri",
        astrenv(:oauth2_endpoint_metadata_op_tos_uri, nil)
      )
      |> put_jar_enabled()
      |> put_jar_metadata_values()
      |> put_oidc_metadata()
      |> astrenv(:oauth2_endpoint_metadata_before_send_resp_callback).()
      |> sign_metadata()

    conn
    |> astrenv(:oauth2_endpoint_metadata_before_send_conn_callback).()
    |> json(metadata)
  end

  @spec maybe_put_authorization_endpoint(map()) :: map()

  defp maybe_put_authorization_endpoint(metadata) do
    if Enum.any?(
         astrenv(:oauth2_grant_types_enabled, []),
         fn grant_type -> OAuth2Utils.uses_authorization_endpoint?(to_string(grant_type)) end
       ) do
      Map.put(
        metadata,
        "authorization_endpoint",
        Routes.authorize_url(AsteroidWeb.Endpoint, :pre_authorize)
      )
    else
      metadata
    end
  end

  @spec maybe_put_token_endpoint(map()) :: map()

  defp maybe_put_token_endpoint(metadata) do
    case astrenv(:oauth2_grant_types_enabled, []) do
      [:implicit] ->
        metadata

      _ ->
        Map.put(
          metadata,
          "token_endpoint",
          Routes.token_url(AsteroidWeb.Endpoint, :handle)
        )
    end
  end

  @spec put_registration_endpoint(map()) :: map()

  defp put_registration_endpoint(metadata) do
    Map.put(
      metadata,
      "registration_endpoint",
      Routes.register_url(AsteroidWeb.Endpoint, :handle)
    )
  end

  @spec put_scopes_supported(map()) :: map()

  defp put_scopes_supported(metadata) do
    grant_types_enabled = astrenv(:oauth2_grant_types_enabled, [])

    scopes =
      if :password in grant_types_enabled do
        OAuth2.Scope.configuration_for_flow(:ropc)[:scopes]
      else
        %{}
      end

    scopes =
      if :client_credentials in grant_types_enabled do
        Map.merge(scopes, OAuth2.Scope.configuration_for_flow(:client_credentials)[:scopes])
      else
        scopes
      end

    scopes =
      if :authorization_code in grant_types_enabled do
        Map.merge(scopes, OAuth2.Scope.configuration_for_flow(:authorization_code)[:scopes])
      else
        scopes
      end

    scopes =
      if :implicit in grant_types_enabled do
        Map.merge(scopes, OAuth2.Scope.configuration_for_flow(:authorization_code)[:scopes])
      else
        scopes
      end

    advertised_scopes =
      Enum.reduce(
        scopes,
        [],
        fn
          {scope, scope_opts}, acc ->
            if scope_opts[:advertise] == false do
              acc
            else
              acc ++ [scope]
            end
        end
      )

    case advertised_scopes do
      [_ | _] ->
        Map.put(metadata, "scopes_supported", advertised_scopes)

      [] ->
        metadata
    end
  end

  @spec put_response_types_supported(map()) :: map()

  defp put_response_types_supported(metadata) do
    case astrenv(:oauth2_response_types_enabled, []) do
      [] ->
        metadata

      response_types when is_list(response_types) ->
        Map.put(metadata, "response_types_supported", Enum.map(response_types, &to_string/1))
    end
  end

  @spec put_grant_types_supported(map()) :: map()

  defp put_grant_types_supported(metadata) do
    case astrenv(:oauth2_grant_types_enabled, []) do
      [] ->
        metadata

      grant_types when is_list(grant_types) ->
        Map.put(metadata, "grant_types_supported", Enum.map(grant_types, &to_string/1))
    end
  end

  @spec put_token_endpoint_auth_method_supported(map()) :: map()

  defp put_token_endpoint_auth_method_supported(metadata) do
    token_endpoint_auth_methods_supported =
      OAuth2.Endpoint.token_endpoint_auth_methods_supported()
      |> Enum.map(&to_string/1)

    case token_endpoint_auth_methods_supported do
      [] ->
        metadata

      methods when is_list(methods) ->
        Map.put(metadata, "token_endpoint_auth_methods_supported", methods)
    end
  end

  @spec put_jwks_uri(map()) :: map()

  defp put_jwks_uri(metadata) do
    if astrenv(:crypto_keys) do
      Map.put(
        metadata,
        "jwks_uri",
        Routes.keys_url(AsteroidWeb.Endpoint, :handle)
      )
    else
      metadata
    end
  end

  @spec put_revocation_endpoint(map()) :: map()

  defp put_revocation_endpoint(metadata) do
    Map.put(
      metadata,
      "revocation_endpoint",
      Routes.revoke_url(AsteroidWeb.Endpoint, :handle)
    )
  end

  @spec put_revocation_endpoint_auth_method_supported(map()) :: map()

  defp put_revocation_endpoint_auth_method_supported(metadata) do
    revoke_endpoint_auth_methods_supported =
      OAuth2.Endpoint.revoke_endpoint_auth_methods_supported()
      |> Enum.map(&to_string/1)

    case revoke_endpoint_auth_methods_supported do
      [] ->
        metadata

      methods when is_list(methods) ->
        Map.put(metadata, "revocation_endpoint_auth_methods_supported", methods)
    end
  end

  @spec put_introspection_endpoint(map()) :: map()

  defp put_introspection_endpoint(metadata) do
    Map.put(
      metadata,
      "introspection_endpoint",
      Routes.introspect_url(AsteroidWeb.Endpoint, :handle)
    )
  end

  @spec put_introspection_endpoint_auth_method_supported(map()) :: map()

  defp put_introspection_endpoint_auth_method_supported(metadata) do
    introspect_endpoint_auth_methods_supported =
      OAuth2.Endpoint.introspect_endpoint_auth_methods_supported()
      |> Enum.map(&to_string/1)

    case introspect_endpoint_auth_methods_supported do
      [] ->
        metadata

      methods when is_list(methods) ->
        Map.put(metadata, "introspection_endpoint_auth_methods_supported", methods)
    end
  end

  @spec put_device_authorization_endpoint(map()) :: map()

  defp put_device_authorization_endpoint(metadata) do
    if :"urn:ietf:params:oauth:grant-type:device_code" in astrenv(:oauth2_grant_types_enabled, []) do
      Map.put(
        metadata,
        "device_authorization_endpoint",
        Routes.device_authorization_url(AsteroidWeb.Endpoint, :handle)
      )
    else
      metadata
    end
  end

  @spec put_code_challenge_methods_supported(map()) :: map()

  defp put_code_challenge_methods_supported(metadata) do
    case astrenv(:oauth2_pkce_policy, []) do
      :disabled ->
        metadata

      _ ->
        methods =
          astrenv(:oauth2_pkce_allowed_methods, [])
          |> Enum.map(&to_string/1)

        Map.put(metadata, "code_challenge_methods_supported", methods)
    end
  end

  @spec put_jar_enabled(map()) :: map()

  defp put_jar_enabled(metadata) do
    case astrenv(:oauth2_jar_enabled, :disabled) do
      :disabled ->
        Map.put(metadata, "request_parameter_supported", false)

      :request_only ->
        Map.put(metadata, "request_parameter_supported", true)

      :request_uri_only ->
        Map.put(metadata, "request_uri_parameter_supported", true)

      :enabled ->
        metadata
        |> Map.put("request_parameter_supported", true)
        |> Map.put("request_uri_parameter_supported", true)
    end
  end

  @spec put_jar_metadata_values(map()) :: map()

  defp put_jar_metadata_values(metadata) do
    case astrenv(:oauth2_jar_enabled, :disabled) do
      :disabled ->
        metadata

      _ ->
        metadata
        |> put_if_not_empty(
          "request_object_encryption_alg_values_supported",
          astrenv(:oauth2_jar_request_object_encryption_alg_values_supported, [])
        )
        |> put_if_not_empty(
          "request_object_encryption_enc_values_supported",
          astrenv(:oauth2_jar_request_object_encryption_enc_values_supported, [])
        )
        |> put_if_not_empty(
          "request_object_signing_alg_values_supported",
          astrenv(:oauth2_jar_request_object_signing_alg_values_supported, [])
        )
    end
  end

  @spec put_oidc_metadata(map()) :: map()

  defp put_oidc_metadata(metadata) do
    if OIDC.enabled?() do
      sig_alg = astrenv(:oidc_endpoint_userinfo_signature_alg_values_supported, [])
      enc_alg = astrenv(:oidc_endpoint_userinfo_encryption_alg_values_supported, [])
      enc_enc = astrenv(:oidc_endpoint_userinfo_encryption_enc_values_supported, [])

      id_token_sig_alg =
        ["RS256" | astrenv(:oidc_id_token_signing_alg_values_supported, [])]
        |> Enum.uniq()

      id_token_enc_alg = astrenv(:oidc_id_token_encryption_alg_values_supported, [])
      id_token_enc_enc = astrenv(:oidc_id_token_encryption_enc_values_supported, [])

      acr_values = Enum.map(astrenv(:oidc_acr_config, []), fn {k, _} -> Atom.to_string(k) end)

      response_modes_supported =
        if astrenv(:oauth2_response_mode_policy, :disabled) == :disabled do
          ["query", "fragment"]
        else
          ["query", "fragment", "form_post"]
        end

      metadata
      |> Map.put("claims_parameter_supported", true)
      |> Map.put("request_parameter_supported", true)
      |> Map.put("request_uri_parameter_supported", true)
      |> Map.put("subject_types_supported", ["public", "pairwise"])
      |> Map.put("userinfo_endpoint", Routes.userinfo_url(AsteroidWeb.Endpoint, :show))
      |> Map.put("response_modes_supported", response_modes_supported)
      |> put_if_not_empty("id_token_signing_alg_values_supported", id_token_sig_alg)
      |> put_if_not_empty("id_token_encryption_alg_values_supported", id_token_enc_alg)
      |> put_if_not_empty("id_token_encryption_enc_values_supported", id_token_enc_enc)
      |> put_if_not_empty("userinfo_signing_alg_values_supported", sig_alg)
      |> put_if_not_empty("userinfo_encryption_alg_values_supported", enc_alg)
      |> put_if_not_empty("userinfo_encryption_enc_values_supported", enc_enc)
      |> put_if_not_empty("acr_values_supported", acr_values)
      |> put_if_not_empty("claims_supported", astrenv(:oidc_claims_supported, []))
      |> put_if_not_empty(
        "display_values_supported",
        astrenv(:oidc_endpoint_metadata_display_values_supported, [])
      )
    else
      metadata
    end
  end

  @spec sign_metadata(map()) :: map()

  defp sign_metadata(metadata) do
    case astrenv(:oauth2_endpoint_metadata_signed_fields, :disabled) do
      :disabled ->
        metadata

      :all ->
        Map.put(metadata, "signed_metadata", signed_statement(metadata))

      fields when is_list(fields) ->
        fields_to_be_signed = Map.take(metadata, fields ++ ["issuer"])

        Map.put(metadata, "signed_metadata", signed_statement(fields_to_be_signed))
    end
  end

  @spec signed_statement(map()) :: String.t()

  defp signed_statement(to_be_signed) do
    signing_key = astrenv(:oauth2_endpoint_metadata_signing_key)
    signing_alg = astrenv(:oauth2_endpoint_metadata_signing_alg)

    {:ok, jwk} = Asteroid.Crypto.Key.get(signing_key)

    if signing_alg do
      jws = JOSE.JWS.from_map(%{"alg" => signing_alg})

      JOSE.JWT.sign(jwk, jws, to_be_signed)
      |> JOSE.JWS.compact()
      |> elem(1)
    else
      JOSE.JWT.sign(jwk, to_be_signed)
      |> JOSE.JWS.compact()
      |> elem(1)
    end
  end
end


<|start_filename|>mix.exs<|end_filename|>
defmodule Asteroid.MixProject do
  use Mix.Project

  def project do
    [
      app: :asteroid,
      version: "0.2.0",
      elixir: "~> 1.5",
      elixirc_paths: elixirc_paths(Mix.env()),
      compilers: [:phoenix, :gettext] ++ Mix.compilers(),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps(Mix.env()),
      docs: docs(),
      dialyzer: [plt_add_apps: [:mix]]
    ]
  end

  # Configuration for the OTP application.
  #
  # Type `mix help compile.app` for more information.
  def application do
    [
      mod: {Asteroid.Application, []},
      extra_applications: [:logger, :runtime_tools]
    ]
  end

  # Specifies which paths to compile per environment.
  defp elixirc_paths(:dev), do: ["lib", "custom_example"]
  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  # Specifies your project dependencies.
  #
  # Type `mix help deps` for examples and options.
  defp deps(_) do
    [
      {:apiac, github: "tanguilp/apiac", tag: "0.3.0"},
      {:apiac_auth_basic, github: "tanguilp/apiac_auth_basic"},
      {:apiac_auth_bearer, github: "tanguilp/apiac_auth_bearer"},
      {:apiac_auth_client_secret_post, github: "tanguilp/apiac_auth_client_secret_post"},
      # {:apiac_auth_mtls, github: "tanguilp/apiac_auth_mtls"},
      {:apiac_filter_ip_blacklist, github: "tanguilp/apiac_filter_ip_blacklist"},
      {:apiac_filter_ip_whitelist, github: "tanguilp/apiac_filter_ip_whitelist"},
      {:apiac_filter_throttler, github: "tanguilp/apiac_filter_throttler"},
      {:oauth2_utils, github: "tanguilp/oauth2_utils", override: true},
      {:attribute_repository, github: "tanguilp/attribute_repository", override: true},
      # {:attribute_repository_ldap, github: "tanguilp/attribute_repository_ldap"},
      {:attribute_repository_mnesia, github: "tanguilp/attribute_repository_mnesia"},
      # {:attribute_repository_riak, github: "tanguilp/attribute_repository_riak"},
      {:content_type, github: "marcelotto/content_type", tag: "master"},
      {:corsica, "~> 1.0"},
      {:dialyxir, "~> 1.0.0-rc.4", only: [:dev], runtime: false},
      {:ex_doc, "~> 0.19", only: :dev, runtime: false},
      {:gettext, "~> 0.11"},
      {:hammer, "~> 6.0"},
      {:jason, "~> 1.0"},
      {:jose, "~> 1.9"},
      {:jwks_uri_updater, github: "tanguilp/jwks_uri_updater"},
      {:oauth2_metadata_updater, github: "tanguilp/oauth2_metadata_updater", override: true},
      {:phoenix, "~> 1.4.0"},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:plug_cowboy, "~> 2.0"},
      {:poison, "~> 4.0", override: true},
      {:riak, github: "tanguilp/riak-elixir-client"},
      {:singleton, "~> 1.2.0"}
    ]
  end

  # Aliases are shortcuts or tasks specific to the current project.
  # For example, to create, migrate and run the seeds file at once:
  #
  #     $ mix ecto.setup
  #
  # See the documentation for `Mix` for more info on aliases.
  defp aliases do
    [
      "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
      "ecto.reset": ["ecto.drop", "ecto.setup"]
      # test: ["ecto.create --quiet", "ecto.migrate", "test"]
    ]
  end

  defp docs() do
    [
      main: "getting-started",
      groups_for_modules: [
        Resources: [Asteroid.Client, Asteroid.Subject, Asteroid.Device],
        Tokens: [
          Asteroid.Token,
          Asteroid.Token.AccessToken,
          Asteroid.Token.RefreshToken,
          Asteroid.Token.AuthorizationCode,
          Asteroid.Token.DeviceCode,
          Asteroid.Token.IDToken
        ],
        "Token stores": [
          Asteroid.ObjectStore,
          Asteroid.ObjectStore.AccessToken,
          Asteroid.ObjectStore.AccessToken.Mnesia,
          Asteroid.ObjectStore.AccessToken.Riak,
          Asteroid.ObjectStore.AuthorizationCode,
          Asteroid.ObjectStore.AuthorizationCode.Mnesia,
          Asteroid.ObjectStore.AuthorizationCode.Riak,
          Asteroid.ObjectStore.AuthenticatedSession,
          Asteroid.ObjectStore.AuthenticatedSession.Mnesia,
          Asteroid.ObjectStore.AuthenticatedSession.Riak,
          Asteroid.ObjectStore.AuthenticationEvent,
          Asteroid.ObjectStore.AuthenticationEvent.Mnesia,
          Asteroid.ObjectStore.AuthenticationEvent.Riak,
          Asteroid.ObjectStore.RefreshToken,
          Asteroid.ObjectStore.RefreshToken.Mnesia,
          Asteroid.ObjectStore.RefreshToken.Riak,
          Asteroid.ObjectStore.DeviceCode,
          Asteroid.ObjectStore.DeviceCode.Mnesia,
          Asteroid.ObjectStore.DeviceCode.Riak,
          Asteroid.ObjectStore.GenericKV,
          Asteroid.ObjectStore.GenericKV.Mnesia,
          Asteroid.ObjectStore.GenericKV.Riak
        ]
      ],
      extras: [
        "guides/getting-started.md",
        "guides/general-architecture.md",
        "guides/general-configuration.md",
        "guides/attribute-repositories.md",
        "guides/object-stores.md",
        "guides/protecting-apis.md",
        "guides/configuring-resources.md",
        "guides/crypto-keys.md",
        "guides/network-configuration.md",
        "guides/configuring-riak.md",
        "guides/customizing.md",
        "guides/running-demo-app.md",
        "guides/oauth2/terminology-conventions.md",
        "guides/oauth2/managing-scopes.md",
        "guides/oauth2/oauth2-core.md",
        "guides/oauth2/token-introspection.md",
        "guides/oauth2/token-revocation.md",
        "guides/oauth2/jwt-access-tokens.md",
        "guides/oauth2/pkce.md",
        "guides/oauth2/dynamic-client-registration.md",
        "guides/oauth2/device-flow.md",
        "guides/oauth2/server-metadata.md",
        "guides/oauth2/jar.md",
        "guides/openid-connect/sessions-loas.md",
        "guides/openid-connect/openid-connect-core.md",
        "guides/openid-connect/oidc-dynamic-client-registration.md",
        "guides/openid-connect/response-mode.md",
        "guides/openid-connect/openid-connect-discovery.md"
      ],
      groups_for_extras: [
        OAuth2: [
          "guides/oauth2/terminology-conventions.md",
          "guides/oauth2/managing-scopes.md",
          "guides/oauth2/oauth2-core.md",
          "guides/oauth2/token-introspection.md",
          "guides/oauth2/token-revocation.md",
          "guides/oauth2/jwt-access-tokens.md",
          "guides/oauth2/pkce.md",
          "guides/oauth2/dynamic-client-registration.md",
          "guides/oauth2/device-flow.md",
          "guides/oauth2/server-metadata.md",
          "guides/oauth2/jar.md"
        ],
        "OpenID Connect": [
          "guides/openid-connect/sessions-loas.md",
          "guides/openid-connect/openid-connect-core.md",
          "guides/openid-connect/oidc-dynamic-client-registration.md",
          "guides/openid-connect/response-mode.md",
          "guides/openid-connect/openid-connect-discovery.md"
        ]
      ]
    ]
  end
end


<|start_filename|>lib/asteroid/subject.ex<|end_filename|>
defmodule Asteroid.Subject do
  use AttributeRepository.Resource, otp_app: :asteroid

  @moduledoc """
  `AttributeRepository.Resource` for subjects

  Subject resource are real-world physical persons, such as the reader of this documentation. It
  refers to the OAuth2 definition of a subject.

  ## Field naming
  The following fields have standardised meaning:
  - `"sub"`: the subject identifier (`"sub"` in OAuth2) (`String.t()`)
  - `"consented_scopes"`: a map whose keys are the `client_id`s and the values the string
  representation of the already consented scopes (such as `"email profile address"`). Note that
  although this is the format used in the demo application, other ways to store consented scopes
  are also possible (but it still has to remain per client)

  ## Configuration

  This modules uses the default configuration of `AttributeRepository.Resource` (see `config/1`).

  ## Security considerations

  - When storing subject passwords, you shall take into account the specifics of such password
  (reuse, non-randomness...) and use the relevant algorithms. If you don't know about this
  topic, you should probably not try to implement it by yourself.

  ## Example

  ```elixir
  iex(13)> alias Asteroid.Subject
  Asteroid.Subject

  iex> {:ok, s} = Subject.load("uid=john,ou=People,dc=example,dc=org")
  {:ok,
   %Asteroid.Subject{
     attrs: %{
       "cn" => ["<NAME>"],
       "displayName" => "<NAME>",
       "givenName" => ["John"],
       "mail" => ["<EMAIL>"],
       "manager" => ["uid=toto,ou=People,dc=example,dc=org"],
       "sn" => ["Doe"]
     },
     id: "uid=john,ou=People,dc=example,dc=org",
     modifications: [],
     newly_created: false
   }}
  iex> s = s
  ...> |> Subject.add("initials", "JD")
  ...> |> Subject.add("mail", "<EMAIL>")
  ...> |> Subject.remove("manager")
  %Asteroid.Subject{
    attrs: %{
      "cn" => ["<NAME>"],
      "displayName" => "<NAME>",
      "givenName" => ["John"],
      "initials" => "JD",
      "mail" => ["<EMAIL>", "<EMAIL>"],
      "sn" => ["Doe"]
    },
    id: "uid=john,ou=People,dc=example,dc=org",
    modifications: [
      {:add, "initials", "JD"},
      {:add, "mail", "<EMAIL>"},
      {:delete, "manager"}
    ],
    newly_created: false
  }
  iex> Subject.store(s)
  :ok
  ```
  """

  def gen_new_id(opts) do
    "sub-" <> super(opts)
  end
end


<|start_filename|>lib/asteroid_web/controllers/discovery/keys_controller.ex<|end_filename|>
defmodule AsteroidWeb.Discovery.KeysController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias Asteroid.Crypto

  def handle(conn, _params) do
    key_list =
      Enum.reduce(
        Crypto.Key.get_all_public(),
        [],
        fn
          jwk_public, acc ->
            case JOSE.JWK.to_public_map(jwk_public) do
              # JOSE returns the private secret for oct keys, hence this match
              {%{kty: :jose_jwk_kty_oct}, _jwk} ->
                acc

              {_key_type, jwk_public_map} ->
                acc ++ [Map.delete(jwk_public_map, "advertise")]
            end
        end
      )
      |> put_keys()
      |> astrenv(:oauth2_endpoint_discovery_keys_before_send_resp_callback).()

    conn
    |> astrenv(:oauth2_endpoint_discovery_keys_before_send_conn_callback).()
    |> json(key_list)
  end

  @spec put_keys([map()]) :: map()

  defp put_keys(key_list) do
    %{"keys" => key_list}
  end
end


<|start_filename|>lib/asteroid.ex<|end_filename|>
defmodule Asteroid do
  @moduledoc false
end


<|start_filename|>lib/asteroid_web/views/layout_view.ex<|end_filename|>
defmodule AsteroidWeb.LayoutView do
  @moduledoc false

  use AsteroidWeb, :view
end


<|start_filename|>lib/asteroid/oauth2/redirect_uri.ex<|end_filename|>
defmodule Asteroid.OAuth2.RedirectUri do
  @moduledoc """
  Helper functions to deal with redirect URIs
  """

  @type t :: String.t()

  @doc """
  Returns `true` if a redirect uri is valid, `false` otherwise
  """

  @spec valid?(String.t()) :: boolean()

  def valid?(redirect_uri) when is_binary(redirect_uri) do
    parsed_uri = URI.parse(redirect_uri)

    if parsed_uri.scheme != nil and parsed_uri.fragment == nil do
      true
    else
      false
    end
  end

  def valid?(_) do
    false
  end

  @doc """
  Add params to a request URI

  A request URI can already contain params, and new params shall be added to it
  without erasing the others. This is what this function does.
  """

  @spec add_params(String.t(), %{required(String.t()) => String.t()}) :: String.t()

  def add_params(redirect_uri, params) do
    case URI.parse(redirect_uri) do
      %URI{query: query} = parsed_uri when is_binary(query) ->
        parsed_uri
        |> Map.put(:query, URI.encode_query(URI.decode_query(query, params)))

      %URI{query: nil} = parsed_uri ->
        parsed_uri
        |> Map.put(:query, URI.encode_query(params))
    end
    |> URI.to_string()
  end
end


<|start_filename|>lib/asteroid/oauth2.ex<|end_filename|>
defmodule Asteroid.OAuth2 do
  @moduledoc """
  Types and helper functions for OAuth2
  """

  import Asteroid.Utils

  defmodule UnsupportedGrantTypeError do
    @moduledoc """
    Error returned when the grant type is unsupported or unknown
    """

    defexception [:grant_type]

    @type t :: %__MODULE__{
            grant_type: String.t()
          }

    @impl true

    def message(%{grant_type: grant_type}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Unsupported grant type `#{grant_type}` (enabled grant types are: " <>
            "#{inspect(astrenv(:oauth2_grant_types_enabled, []))})"

        :normal ->
          "Unsupported grant type `#{grant_type}`"

        :minimal ->
          ""
      end
    end
  end

  defmodule UnsupportedResponseTypeError do
    @moduledoc """
    Error returned when the grant type is unsupported or unknown
    """

    defexception [:response_type]

    @type t :: %__MODULE__{
            response_type: String.t()
          }

    @impl true

    def message(%{response_type: response_type}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Unsupported response type `#{response_type} (enabled grant types are: " <>
            "#{inspect(astrenv(:oauth2_response_types_enabled, []))})"

        :normal ->
          "Unsupported response type `#{response_type}`"

        :minimal ->
          ""
      end
    end
  end

  defmodule InvalidGrantError do
    @moduledoc """
    Error returned for invalid grants (refresh token, password, authorization code,
    PKCE code verifier...)
    """

    @enforce_keys [:grant, :reason]

    defexception [:grant, :reason, :debug_details]

    @type t :: %__MODULE__{
            grant: String.t(),
            reason: String.t(),
            debug_details: String.t()
          }

    @impl true

    def message(%{grant: grant, reason: reason, debug_details: debug_details}) do
      case astrenv(:api_error_response_verbosity) do
        :debug when not is_nil(debug_details) ->
          "Invalid grant `#{grant}`: #{reason} (#{debug_details})"

        :debug when is_nil(debug_details) ->
          "Invalid grant `#{grant}`: #{reason}"

        :normal ->
          "Invalid grant `#{grant}`: #{reason}"

        :minimal ->
          ""
      end
    end
  end

  defmodule AccessDeniedError do
    @moduledoc """
    Error returned when the access was denied either because of the user not consenting or
    the server's policy inadequation with the request (eg. scopes)
    """

    @enforce_keys [:reason]

    defexception [:reason]

    @type t :: %__MODULE__{
            reason: String.t()
          }

    @impl true

    def message(%{reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Access denied: " <> reason

        :normal ->
          "Access denied: " <> reason

        :minimal ->
          ""
      end
    end
  end

  defmodule ServerError do
    @moduledoc """
    Error returned in case of undetermined server error
    """

    @enforce_keys [:reason]

    defexception [:reason, :stacktrace]

    @type t :: %__MODULE__{
            reason: String.t(),
            stacktrace: Exception.stacktrace()
          }

    @impl true

    def message(%{reason: reason} = e) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Server error: #{reason} (stacktrace: #{inspect(e.stacktrace)})"

        :normal ->
          "Server error: " <> reason

        :minimal ->
          ""
      end
    end
  end

  defmodule TemporarilyUnavailableError do
    @moduledoc """
    Error returned when the server is unavailable
    """

    @enforce_keys [:reason]

    defexception [:reason]

    @type t :: %__MODULE__{
            reason: String.t()
          }

    @impl true

    def message(%{reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "Temporary unavailable: " <> reason

        :normal ->
          "Temporary unavailable: " <> reason

        :minimal ->
          ""
      end
    end
  end

  @typedoc """
  Protocol in use
  """

  @type protocol :: :oauth2 | :oidc

  @typedoc """
  OAuth2 client_id
  """

  @type client_id :: String.t()

  @typedoc """
  OAuth2 subject
  """

  @type subject :: String.t()

  @typedoc """
  OAuth2 issuer
  """

  @type issuer :: String.t()

  @typedoc """
  OAuth2 audience
  """

  @type audience :: String.t()

  @typedoc """
  OAuth2 grant types
  """

  @type grant_type ::
          :authorization_code
          | :implicit
          | :password
          | :client_credentials
          | :refresh_token
          | :"urn:ietf:params:oauth:grant-type:device_code"

  @typedoc """
  String representation of `t:grant_type/0`

  Must be the string conversion of its corresponding `t:grant_type/0` atom.
  """

  @type grant_type_str :: String.t()

  @type flow ::
          :ropc
          | :client_credentials
          | :authorization_code
          | :implicit
          | :device_authorization
          | :oidc_authorization_code
          | :oidc_implicit
          | :oidc_hybrid

  @typedoc """
  String representation of a `t:flow()/0`

  Must be the string conversion of its corresponding `t:flow/0` atom.
  """

  @type flow_str :: String.t()

  @type response_type ::
          :code
          | :token
          | :id_token
          | :"id_token token"
          | :"code id_token"
          | :"code token"
          | :"code id_token token"

  @typedoc """
  String representation of a `t:response_type/0`

  Must be the string conversion of its corresponding `t:response_type/0`
  """

  @type response_type_str :: String.t()

  @type response_mode ::
          :query
          | :fragment
          | :form_post

  @typedoc """
  String representation of a `t:response_mode/0`

  Must be the string conversion of its corresponding `t:response_mode/0`
  """

  @type response_mode_str :: String.t()

  @typedoc """
  Atoms describing the endpoints

  The values refer to:
  - `:authorize`: `/authorize`
  - `:device`: `/device`
  - `:token`: `/api/oauth2/token`
  - `:introspect`: `/api/oauth2/introspect`
  - `:revoke`: `/api/oauth2/revoke`
  - `:register`: `/api/oauth2/register`
  - `:device_authorization`: `/api/oauth2/device_authorization`
  """

  # web flow for the authorization code or implicit flow
  @type endpoint ::
          :authorize
          # web flow for the device authorization flow
          | :device
          | :token
          | :introspect
          | :revoke
          | :register
          | :device_authorization

  @doc """
  Converts a `t:grant_type_str/0` to a `t:grant_type/0`

  Returns `{:ok, grant_type()}` if the grant type is supported,
  `{:error, %Asteroid.OAuth2.UnsupportedGrantTypeError{}}` otherwise.
  """

  @spec to_grant_type(String.t()) ::
          {:ok, grant_type()}
          | {:error, %__MODULE__.UnsupportedGrantTypeError{}}

  def to_grant_type("authorization_code"), do: {:ok, :authorization_code}
  def to_grant_type("implicit"), do: {:ok, :implicit}
  def to_grant_type("password"), do: {:ok, :password}
  def to_grant_type("client_credentials"), do: {:ok, :client_credentials}
  def to_grant_type("refresh_token"), do: {:ok, :refresh_token}

  def to_grant_type(param),
    do: {:error, __MODULE__.UnsupportedGrantTypeError.exception(grant_type: param)}

  @doc """
  Converts a `t:grant_type_str/0` to a `t:grant_type/0`

  Returns `t:grant_type/0` if it exists or raises a
  `%Asteroid.OAuth2.UnsupportedGrantTypeError{}` otherwise.
  """

  @spec to_grant_type!(String.t()) :: grant_type()

  def to_grant_type!(grant_type_str) do
    case to_grant_type(grant_type_str) do
      {:ok, grant_type} ->
        grant_type

      {:error, e} ->
        raise e
    end
  end

  @doc """
  Returns the flow associated to a grant type, or `nil`

  The conversion is performed in conformance to the following table:

  |      Grant type        |       Flow            |
  |:----------------------:|:---------------------:|
  | `:authorization_code`  | `:authorization_code` |
  | `:implicit`            | `:implicit`           |
  | `:password`            | `:ropc`               |
  | `:client_credentials`  | `:client_credentials` |

  """

  @spec grant_type_to_flow(grant_type()) :: flow() | nil

  def grant_type_to_flow(:authorization_code), do: :authorization_code
  def grant_type_to_flow(:implicit), do: :implicit
  def grant_type_to_flow(:password), do: :ropc
  def grant_type_to_flow(:client_credentials), do: :client_credentials
  def grant_type_to_flow(_), do: nil

  @doc """
  Returns the flow from the response type and the protocol
  """

  @spec response_type_to_flow(response_type_str(), protocol()) ::
          {:ok, flow()}
          | {:error, %UnsupportedResponseTypeError{}}

  def response_type_to_flow("code", :oauth2), do: {:ok, :authorization_code}
  def response_type_to_flow("token", :oauth2), do: {:ok, :implicit}
  def response_type_to_flow("code", :oidc), do: {:ok, :oidc_authorization_code}
  def response_type_to_flow("id_token", :oidc), do: {:ok, :oidc_implicit}
  def response_type_to_flow("id_token token", :oidc), do: {:ok, :oidc_implicit}
  def response_type_to_flow("code id_token", :oidc), do: {:ok, :oidc_hybrid}
  def response_type_to_flow("code token", :oidc), do: {:ok, :oidc_hybrid}
  def response_type_to_flow("code id_token token", :oidc), do: {:ok, :oidc_hybrid}

  def response_type_to_flow(val, _),
    do: {:error, UnsupportedResponseTypeError.exception(response_type: val)}

  @doc """
  Converts a `t:response_type_str/0` to a `t:response_type/0`

  Returns `{:ok, response_type()}` if the response type is supported,
  `{:error, %Asteroid.OAuth2.UnsupportedResponseTypeError{}}` otherwise.
  """

  @spec to_response_type(String.t()) ::
          {:ok, response_type()}
          | {:error, %UnsupportedResponseTypeError{}}

  def to_response_type("code"), do: {:ok, :code}
  def to_response_type("token"), do: {:ok, :token}
  def to_response_type("id_token"), do: {:ok, :id_token}
  def to_response_type("id_token token"), do: {:ok, :"id_token token"}
  def to_response_type("code id_token"), do: {:ok, :"code id_token"}
  def to_response_type("code token"), do: {:ok, :"code token"}
  def to_response_type("code id_token token"), do: {:ok, :"code id_token token"}

  def to_response_type(val),
    do: {:error, UnsupportedResponseTypeError.exception(response_type: val)}

  @doc """
  Converts a `t:response_type_str/0` to a `t:response_type/0`

  Returns `t:response_type/0` if it exists or raises a
  `%Asteroid.OAuth2.UnsupportedResponseTypeError{}` otherwise.
  """

  @spec to_response_type!(String.t()) :: response_type()

  def to_response_type!(response_type_str) do
    case to_response_type(response_type_str) do
      {:ok, response_type} ->
        response_type

      {:error, e} ->
        raise e
    end
  end

  @doc """
  Returns `:ok` if the grant type is enabled,
  `{:error, %Asteroid.OAuth2.UnsupportedGrantTypeError{}}` otherwise

  Uses the #{Asteroid.Config.link_to_option(:oauth2_grant_types_enabled)} configuration
  option's value.
  """

  @spec grant_type_enabled?(grant_type()) ::
          :ok
          | {:error, %UnsupportedGrantTypeError{}}

  def grant_type_enabled?(grant_type) do
    if grant_type in astrenv(:oauth2_grant_types_enabled, []) do
      :ok
    else
      {:error, UnsupportedGrantTypeError.exception(grant_type: Atom.to_string(grant_type))}
    end
  end

  @doc """
  Returns `:ok` if the grant type is enabled,
  `{:error, %Asteroid.OAuth2.UnsupportedResponseTypeError}` otherwise

  Uses the #{Asteroid.Config.link_to_option(:oauth2_response_types_enabled)} configuration
  option's value.
  """

  @spec response_type_enabled?(response_type()) ::
          :ok
          | {:error, %UnsupportedResponseTypeError{}}

  def response_type_enabled?(response_type) do
    if response_type in astrenv(:oauth2_response_types_enabled, []) do
      :ok
    else
      {:error,
       UnsupportedResponseTypeError.exception(response_type: Atom.to_string(response_type))}
    end
  end

  @doc """
  Converts a `t:flow_str/0` to a `t:flow/0`
  """

  @spec to_flow(flow_str()) :: flow()

  def to_flow("ropc"), do: :ropc
  def to_flow("client_credentials"), do: :client_credentials
  def to_flow("authorization_code"), do: :authorization_code
  def to_flow("oidc_authorization_code"), do: :oidc_authorization_code
  def to_flow("oidc_implicit"), do: :oidc_implicit
  def to_flow("oidc_hybrid"), do: :oidc_hybrid

  @doc """
  Returns the issuer

  The issuer is the concatenation of the url and the base path (in case there is one, for
  instance when using Asteroid behing a reverse proxy).
  """

  @spec issuer() :: String.t()

  def issuer() do
    AsteroidWeb.Router.Helpers.url(AsteroidWeb.Endpoint)
  end

  @doc """
  Returns the default response mode for a flow
  """

  @spec default_response_mode(flow()) :: response_mode()

  def default_response_mode(:authorization_code), do: :query
  def default_response_mode(:implicit), do: :fragment
  def default_response_mode(:oidc_authorization_code), do: :query
  def default_response_mode(:oidc_implicit), do: :fragment
  def default_response_mode(:oidc_hybrid), do: :fragment
end


<|start_filename|>lib/asteroid_web/controllers/api/request_object_controller.ex<|end_filename|>
defmodule AsteroidWeb.API.RequestObjectController do
  @moduledoc false

  use AsteroidWeb, :controller

  import Asteroid.Utils

  alias Asteroid.OAuth2
  alias AsteroidWeb.Router.Helpers, as: Routes

  def show(conn, %{"id" => key}) do
    case OAuth2.JAR.get_stored_request_object(key) do
      {:ok, request_object} ->
        conn
        |> put_status(200)
        |> text(request_object)

      {:error, _} ->
        conn
        # FIXME: verbose level
        |> send_resp(404, "")
    end
  end

  def create(conn, %{"request_object" => request_object}) do
    key = secure_random_b64()

    to_store = %{
      "request_object" => request_object,
      "exp" => now() + astrenv(:oauth2_jar_request_object_lifetime)
    }

    case OAuth2.JAR.put_request_object(key, to_store) do
      :ok ->
        conn
        |> put_resp_header(
          "location",
          Routes.request_object_url(AsteroidWeb.Endpoint, :show, key)
        )
        |> put_resp_content_type("application/jwt")
        |> send_resp(201, "")

      {:error, _} ->
        conn
        # FIXME: verbose level
        |> send_resp(500, "")
    end
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oauth2/revoke_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.RevokeControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Token.{RefreshToken, AccessToken}
  alias AsteroidWeb.Router.Helpers, as: Routes

  ##########################################################################
  # General tests
  ##########################################################################

  test "invalid content-type", %{conn: conn} do
    assert_raise Plug.Parsers.UnsupportedMediaTypeError, fn ->
      conn
      |> put_req_header("content-type", "plain/text")
      |> post(Routes.revoke_path(conn, :handle), "Some plain text")
      |> json_response(400)
    end
  end

  test "missing token parameter", %{conn: conn} do
    req_body = %{
      "other" => "parameter"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.revoke_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "invalid token parameter", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.revoke_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "invalid token_type_hint parameter", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>",
      "token_type_hint" => "unknown"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.revoke_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "unsupported_token_type"
  end

  test "no credentials for confidential client", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    conn = post(conn, Routes.revoke_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  # next 2 tests testing that:
  #
  # If the
  # client attempted to authenticate via the "Authorization"
  # request header field, the authorization server MUST
  # respond with an HTTP 401 (Unauthorized) status code and
  # include the "WWW-Authenticate" response header field
  # matching the authentication scheme used by the client.
  #
  # from https://tools.ietf.org/html/rfc6749#section-5.2
  test "invalid basic credentials for confidential client", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("invalid_client", "secret"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm=)

    refute Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer )

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "public client with no credentials forbidden", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>urmfaiy"
    }

    conn = post(conn, Routes.revoke_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  ##########################################################################
  # Endpoint test - access tokens
  ##########################################################################

  test "existing access token, no token_type_hint param", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert response(conn, 200) == ""
    assert {:error, _} = AccessToken.get(access_token.id)
  end

  test "existing access token, correct token_type_hint param", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "access_token"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert response(conn, 200) == ""
    assert {:error, _} = AccessToken.get(access_token.id)
  end

  test "existing access token, incorrect token_type_hint param", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "refresh_token"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert response(conn, 200) == ""
    assert {:error, _} = AccessToken.get(access_token.id)
  end

  test "exsiting access token, issued to another client", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_2")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id
    }

    conn
    |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
    |> post(Routes.revoke_path(conn, :handle), req_body)
    |> response(200)

    assert {:ok, _} = AccessToken.get(access_token.id)
  end

  ##########################################################################
  # Endpoint test - refresh tokens
  ##########################################################################

  test "existing refresh token, no token_type_hint param", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert response(conn, 200) == ""
    assert {:error, _} = RefreshToken.get(refresh_token.id)
  end

  test "existing refresh token, correct token_type_hint param", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "refresh_token"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert response(conn, 200) == ""
    assert {:error, _} = RefreshToken.get(refresh_token.id)
  end

  test "existing refresh token, incorrect token_type_hint param", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "access_token"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.revoke_path(conn, :handle), req_body)

    assert response(conn, 200)
    assert {:error, _} = RefreshToken.get(refresh_token.id)
  end

  test "exsiting refresh token, issued to another client", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("client_id", "client_confidential_2")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id
    }

    conn
    |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
    |> post(Routes.revoke_path(conn, :handle), req_body)
    |> response(200)

    assert {:ok, _} = RefreshToken.get(refresh_token.id)
  end

  ##########################################################################
  # Helper functions
  ##########################################################################

  defp basic_auth_header(client, secret) do
    "Basic " <> Base.encode64(client <> ":" <> secret)
  end
end


<|start_filename|>test/asteroid_web/controllers/well_known/oauth_authorization_server_test.exs<|end_filename|>
defmodule AsteroidWeb.WellKnown.OauthAuthorizationServerControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias AsteroidWeb.Router.Helpers, as: Routes
  alias AsteroidWeb.Endpoint
  alias AsteroidWeb.Endpoint
  alias Asteroid.OAuth2
  alias OAuth2Utils.Scope

  test "verifiy all fields", %{conn: conn} do
    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert response["issuer"] == OAuth2.issuer()
    assert response["authorization_endpoint"] == Routes.authorize_url(Endpoint, :pre_authorize)
    assert response["token_endpoint"] == Routes.token_url(Endpoint, :handle)
    assert response["jwks_uri"] == Routes.keys_url(Endpoint, :handle)

    assert response["registration_endpoint"] ==
             Routes.register_url(Endpoint, :handle)

    assert Enum.sort(response["scopes_supported"]) ==
             Scope.Set.new()
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:ropc))
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:client_credentials))
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:authorization_code))
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:implicit))
             |> Scope.Set.to_list()
             |> Enum.sort()

    assert Enum.sort(response["response_types_supported"]) ==
             astrenv(:oauth2_response_types_enabled)
             |> Enum.map(&to_string/1)
             |> Enum.sort()

    assert Enum.sort(response["grant_types_supported"]) ==
             astrenv(:oauth2_grant_types_enabled)
             |> Enum.map(&to_string/1)
             |> Enum.sort()

    assert Enum.sort(response["token_endpoint_auth_methods_supported"]) ==
             OAuth2.Endpoint.token_endpoint_auth_methods_supported()
             |> Enum.map(&to_string/1)
             |> Enum.sort()

    assert response["service_documentation"] == nil
    assert response["ui_locales_supported"] == nil
    assert response["op_policy_uri"] == nil
    assert response["op_tos_uri"] == nil
    assert response["revocation_endpoint"] == Routes.revoke_url(Endpoint, :handle)

    assert Enum.sort(response["revocation_endpoint_auth_methods_supported"]) ==
             OAuth2.Endpoint.revoke_endpoint_auth_methods_supported()
             |> Enum.map(&to_string/1)
             |> Enum.sort()

    assert response["introspection_endpoint"] == Routes.introspect_url(Endpoint, :handle)

    assert Enum.sort(response["introspection_endpoint_auth_methods_supported"]) ==
             OAuth2.Endpoint.introspect_endpoint_auth_methods_supported()
             |> Enum.map(&to_string/1)
             |> Enum.sort()

    assert Enum.sort(response["code_challenge_methods_supported"]) ==
             astrenv(:oauth2_pkce_allowed_methods)
             |> Enum.map(&to_string/1)
             |> Enum.sort()
  end

  test "authorization endpoint not needed", %{conn: conn} do
    Process.put(:oauth2_grant_types_enabled, [:client_credentials, :password])

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert response["authorization_endpoint"] == nil
  end

  test "token endpoint not needed", %{conn: conn} do
    Process.put(:oauth2_grant_types_enabled, [:implicit])

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert response["token_endpoint"] == nil
  end

  test "scope not advertised", %{conn: conn} do
    Process.put(:oauth2_scope_config, scopes: %{"scp6" => [advertise: false]})

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert Enum.sort(response["scopes_supported"]) ==
             Scope.Set.new()
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:ropc))
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:client_credentials))
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:authorization_code))
             |> Scope.Set.union(OAuth2.Scope.scopes_for_flow(:implicit))
             |> Scope.Set.to_list()
             |> Kernel.--(["scp6"])
             |> Enum.sort()
  end

  test "non automatic fields can be set", %{conn: conn} do
    Process.put(:oauth2_endpoint_metadata_service_documentation, "aaa")
    Process.put(:oauth2_endpoint_metadata_ui_locales_supported, "bbb")
    Process.put(:oauth2_endpoint_metadata_op_policy_uri, "ccc")
    Process.put(:oauth2_endpoint_metadata_op_tos_uri, "ddd")

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert response["service_documentation"] == "aaa"
    assert response["ui_locales_supported"] == "bbb"
    assert response["op_policy_uri"] == "ccc"
    assert response["op_tos_uri"] == "ddd"
  end

  test "PKCE methods not set if disabled", %{conn: conn} do
    Process.put(:oauth2_pkce_policy, :disabled)

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert response["code_challenge_methods_supported"] == nil
  end

  test "jwks_uri not set if disabled", %{conn: conn} do
    Process.put(:crypto_keys, nil)

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    assert response["jwks_uri"] == nil
  end

  test "signed_metadata published with issuer and correct signature", %{conn: conn} do
    Process.put(
      :oauth2_endpoint_metadata_signed_fields,
      ["token_endpoint", "token_endpoint_auth_methods_supported", "scopes_supported"]
    )

    Process.put(:oauth2_endpoint_metadata_signing_key, "key_auto_sig")
    Process.put(:oauth2_endpoint_metadata_signing_alg, "PS512")

    response =
      conn
      |> get(Routes.oauth_authorization_server_path(conn, :handle))
      |> json_response(200)

    {:ok, jwk} = Asteroid.Crypto.Key.get("key_auto_sig")
    jwk = JOSE.JWK.to_public(jwk)

    assert {true, signed_metadata_str, _} =
             JOSE.JWS.verify_strict(jwk, ["PS512"], response["signed_metadata"])

    signed_metadata = Jason.decode!(signed_metadata_str)

    assert signed_metadata["issuer"] == OAuth2.issuer()
  end
end


<|start_filename|>lib/asteroid/object_store/access_token/mnesia.ex<|end_filename|>
defmodule Asteroid.ObjectStore.AccessToken.Mnesia do
  @moduledoc """
  Mnesia implementation of the `Asteroid.ObjectStore.AccessToken` behaviour

  ## Options
  The options (`Asteroid.ObjectStore.AccessToken.opts()`) are:
  - `:table_name`: an `atom()` for the table name. Defaults to `:asteroid_access_token`
  - `:tab_def`: Mnesia's table definitions of the `:mnesia.create_table/2` function. Defaults to
  the options below. User-defined `:tab_def` will be merged on a key basis, i.e. defaults will
  not be erased. One can use it to add additional indexes for clients or devices, e.g.:
  `tab_def: [index: :refresh_token, :subject_id, :client_id]`
  - `:purge_interval`: the `integer()` interval in seconds the purge process will be triggered,
  or `:no_purge` to disable purge. Defaults to `300` (5 minutes)

  ## Default Mnesia table definition
  ```elixir
  [
    attributes: [:id, :refresh_token_id, :subject_id, :client_id, :device_id, :data],
    index: [:refresh_token_id]
  ]
  ```

  ## Purge process
  The purge process uses the `Singleton` library. Therefore the purge process will be unique
  per cluster (and that's probably what you want if you use Mnesia).
  """

  require Logger

  alias Asteroid.Token.AccessToken

  @behaviour Asteroid.ObjectStore.AccessToken

  @impl true

  def install(opts) do
    :mnesia.stop()

    :mnesia.create_schema([node()])

    :mnesia.start()

    table_name = opts[:table_name] || :asteroid_access_token

    tab_def =
      [
        attributes: [:id, :refresh_token_id, :subject_id, :client_id, :device_id, :data],
        index: [:refresh_token_id]
      ]
      |> Keyword.merge(opts[:tab_def] || [])

    case :mnesia.create_table(table_name, tab_def) do
      {:atomic, :ok} ->
        Logger.info("#{__MODULE__}: created access token store #{table_name}")

        :ok

      {:aborted, {:already_exists, _}} ->
        Logger.info("#{__MODULE__}: access token store #{table_name} already exists")
        :ok

      {:aborted, reason} ->
        Logger.error(
          "#{__MODULE__}: failed to create access token store #{table_name} " <>
            "(reason: #{inspect(reason)})"
        )

        {:error, reason}
    end
  end

  @impl true

  def start_link(opts) do
    case :mnesia.start() do
      :ok ->
        opts = Keyword.merge([purge_interval: 300], opts)

        # we launch the process anyway because we need to return a process
        # but the singleton will do nothing if the value is `:no_purge`
        Singleton.start_child(__MODULE__.Purge, opts, __MODULE__)

      {:error, _} = error ->
        error
    end
  end

  @impl true

  def get(access_token_id, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    case :mnesia.dirty_read(table_name, access_token_id) do
      [] ->
        Logger.debug(
          "#{__MODULE__}: getting access token `#{access_token_id}`, " <> "value: `nil`"
        )

        {:ok, nil}

      [
        {^table_name, ^access_token_id, refresh_token_id, _subject_id, _client_id, _device_id,
         data}
      ] ->
        access_token =
          AccessToken.new(
            id: access_token_id,
            refresh_token_id: refresh_token_id,
            data: data
          )

        Logger.debug(
          "#{__MODULE__}: getting access token `#{access_token_id}`, " <>
            "value: `#{inspect(access_token)}`"
        )

        {:ok, access_token}

      _ ->
        {:error, "Multiple results from Mnesia"}
    end
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_refresh_token_id(refresh_token_id, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    {:ok,
     for {_table_name, access_token_id, _refresh_token_id, _subject_id, _client_id, _device_id,
          _data} <- :mnesia.dirty_match_object({table_name, :_, refresh_token_id, :_, :_, :_, :_}) do
       access_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_subject_id(subject_id, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    {:ok,
     for {_table_name, access_token_id, _refresh_token_id, _subject_id, _client_id, _device_id,
          _data} <- :mnesia.dirty_match_object({table_name, :_, :_, subject_id, :_, :_, :_}) do
       access_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_client_id(client_id, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    {:ok,
     for {_table_name, access_token_id, _refresh_token_id, _subject_id, _client_id, _device_id,
          _data} <- :mnesia.dirty_match_object({table_name, :_, :_, :_, client_id, :_, :_}) do
       access_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def get_from_device_id(device_id, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    {:ok,
     for {_table_name, access_token_id, _refresh_token_id, _subject_id, _client_id, _device_id,
          _data} <- :mnesia.dirty_match_object({table_name, :_, :_, :_, :_, device_id, :_}) do
       access_token_id
     end}
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def put(access_token, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    record = {
      table_name,
      access_token.id,
      access_token.refresh_token_id,
      access_token.data["sub"],
      access_token.data["client_id"],
      access_token.data["device_id"],
      access_token.data
    }

    :mnesia.dirty_write(table_name, record)

    Logger.debug(
      "#{__MODULE__}: stored access token `#{access_token.id}`, " <>
        "value: `#{inspect(access_token)}`"
    )

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end

  @impl true

  def delete(access_token_id, opts) do
    table_name = opts[:table_name] || :asteroid_access_token

    :mnesia.dirty_delete(table_name, access_token_id)

    Logger.debug("#{__MODULE__}: deleted access token `#{access_token_id}`")

    :ok
  catch
    :exit, reason ->
      {:error, reason}
  end
end


<|start_filename|>lib/asteroid/token/id_token.ex<|end_filename|>
defmodule Asteroid.Token.IDToken do
  import Asteroid.Utils

  alias Asteroid.Context
  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OAuth2
  alias Asteroid.OIDC
  alias Asteroid.Subject

  @moduledoc """
  OpenID Connect's ID token structure
  """

  @client_id_token_crypto_fields [
    "id_token_signed_response_alg",
    "id_token_encrypted_response_alg",
    "id_token_encrypted_response_enc"
  ]

  @enforce_keys [:iss, :sub, :aud, :exp, :iat, :client]

  defstruct [
    :iss,
    :sub,
    :aud,
    :exp,
    :iat,
    :auth_time,
    :nonce,
    :acr,
    :amr,
    :azp,
    :client,
    :associated_access_token_serialized,
    :associated_authorization_code_serialized,
    data: %{}
  ]

  @typedoc """
  Structure for the ID token, before it is serialized

  The data in the `"data"` field are added to the serialized (signed) ID token.
  """

  @type t :: %__MODULE__{
          iss: OAuth2.issuer(),
          sub: OAuth2.subject(),
          aud: OAuth2.audience(),
          exp: non_neg_integer(),
          iat: non_neg_integer(),
          auth_time: non_neg_integer() | nil,
          nonce: OIDC.nonce() | nil,
          acr: OIDC.acr() | nil,
          amr: OIDC.amr() | nil,
          azp: String.t() | nil,
          client: Client.t(),
          associated_access_token_serialized: String.t() | nil,
          associated_authorization_code_serialized: String.t() | nil,
          data: map()
        }

  @type id :: String.t()

  @doc """
  Puts a value into the `data` field of an ID token

  If the value is `nil`, the ID token is not changed and the filed is not added.
  """

  @spec put_value(t(), any(), any()) :: t()

  def put_value(id_token, _key, nil), do: id_token

  def put_value(id_token, key, val) do
    %{id_token | data: Map.put(id_token.data, key, val)}
  end

  @doc """
  Removes a value from the `data` field of an ID token

  If the value does not exist, does nothing.
  """

  @spec delete_value(t(), any()) :: t()

  def delete_value(id_token, key) do
    %{id_token | data: Map.delete(id_token.data, key)}
  end

  @doc """
  Serializes the ID token

  If the signing algorithm was set, it uses this algorithm, otherwise it uses the default
  signer of `JOSE.JWT.sign/2`
  """

  @spec serialize(%__MODULE__{}) :: String.t()

  def serialize(id_token) do
    client = Client.fetch_attributes(id_token.client, @client_id_token_crypto_fields)

    jwt =
      (id_token.data || %{})
      |> Map.put("iss", id_token.iss)
      |> Map.put("sub", id_token.sub)
      |> Map.put("aud", id_token.aud)
      |> Map.put("exp", id_token.exp)
      |> Map.put("iat", id_token.iat)
      |> put_if_not_nil("auth_time", id_token.auth_time)
      |> put_if_not_nil("nonce", id_token.nonce)
      |> put_if_not_nil("acr", id_token.acr)
      |> put_if_not_nil("amr", id_token.amr)
      |> put_if_not_nil("azp", id_token.azp)
      |> put_if_not_nil(
        "at_hash",
        token_hash(id_token, id_token.associated_access_token_serialized)
      )
      |> put_if_not_nil(
        "c_hash",
        token_hash(id_token, id_token.associated_authorization_code_serialized)
      )

    signing_alg = client.attrs["id_token_signed_response_alg"] || "RS256"

    eligible_jwks =
      Crypto.Key.get_all()
      |> Enum.filter(fn
        %JOSE.JWK{fields: fields} ->
          fields["use"] == "sig" and
            (fields["key_ops"] in ["sign"] or fields["key_ops"] == nil) and
            (fields["alg"] == signing_alg or fields["alg"] == nil)
      end)

    case eligible_jwks do
      [jwk | _] ->
        serialized_jws =
          JOSE.JWT.sign(jwk, JOSE.JWS.from_map(%{"alg" => signing_alg}), jwt)
          |> JOSE.JWS.compact()
          |> elem(1)

        encryption_alg = client.attrs["id_token_encrypted_response_alg"]

        if encryption_alg do
          case Client.get_jwks(client) do
            {:ok, keys} ->
              eligible_jwks =
                keys
                |> Enum.map(&JOSE.JWK.from/1)
                |> Enum.filter(fn
                  %JOSE.JWK{fields: fields} ->
                    (fields["use"] == "enc" or fields["use"] == nil) and
                      (fields["key_ops"] == "encrypt" or fields["key_ops"] == nil) and
                      (fields["alg"] == encryption_alg or fields["alg"] == nil)
                end)

              case eligible_jwks do
                [jwk | _] ->
                  encryption_enc =
                    client.attrs["id_token_encrypted_response_enc"] || "A128CBC-HS256"

                  JOSE.JWE.block_encrypt(
                    jwk,
                    serialized_jws,
                    %{"alg" => encryption_alg, "enc" => encryption_enc}
                  )
                  |> JOSE.JWE.compact()
                  |> elem(1)

                [] ->
                  raise Crypto.Key.NoSuitableKeyError
              end

            {:error, _} ->
              raise Crypto.Key.NoSuitableKeyError
          end
        else
          serialized_jws
        end

      [] ->
        raise Crypto.Key.NoSuitableKeyError
    end
  end

  @spec token_hash(t(), token :: String.t() | nil) :: String.t()

  defp token_hash(_, nil) do
    nil
  end

  defp token_hash(%__MODULE__{client: client}, token) do
    client = Client.fetch_attributes(client, ["id_token_signed_response_alg"])

    hash_alg =
      cond do
        client.attrs["id_token_signed_response_alg"] in ["ES256", "HS256", "PS256", "RS256"] ->
          :sha256

        client.attrs["id_token_signed_response_alg"] in ["ES384", "HS384", "PS384", "RS384"] ->
          :sha384

        client.attrs["id_token_signed_response_alg"] in ["ES512", "HS512", "PS512", "RS512"] ->
          :sha512

        true ->
          nil
      end

    if hash_alg do
      digest = :crypto.hash(hash_alg, token)

      digest
      |> :binary.part({0, div(byte_size(digest), 2)})
      |> Base.url_encode64(padding: false)
    end
  end

  @doc """
  Returns the ID token lifetime

  ## Processing rules
  - If the client has the following field set to an integer value for the corresponding flow
  returns that value:
    - `"__asteroid_oidc_flow_authorization_code_id_token_lifetime"`
    - `"__asteroid_oidc_flow_implicit_id_token_lifetime"`
    - `"__asteroid_oidc_flow_hybrid_id_token_lifetime"`
  - Otherwise, if the following configuration option is set to an integer for the corresponding
  flow, returns its value:
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_id_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_implicit_id_token_lifetime)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_id_token_lifetime)}
  - Otherwise uses the
  #{Asteroid.Config.link_to_option(:oidc_id_token_lifetime)} configuration option
  - Otherwise returns `0`
  """

  @spec lifetime(Context.t()) :: non_neg_integer()

  def lifetime(%{flow: flow, client: client}) do
    attr =
      case flow do
        :oidc_authorization_code ->
          "__asteroid_oidc_flow_authorization_code_id_token_lifetime"

        :oidc_implicit ->
          "__asteroid_oidc_flow_implicit_id_token_lifetime"

        :oidc_hybrid ->
          "__asteroid_oidc_flow_hybrid_id_token_lifetime"
      end

    client = Client.fetch_attributes(client, [attr])

    case client.attrs[attr] do
      lifetime when is_integer(lifetime) ->
        lifetime

      _ ->
        conf_opt =
          case flow do
            :oidc_authorization_code ->
              :oidc_flow_authorization_code_id_token_lifetime

            :oidc_implicit ->
              :oidc_flow_implicit_id_token_lifetime

            :oidc_hybrid ->
              :oidc_flow_hybrid_id_token_lifetime
          end

        astrenv(conf_opt, astrenv(:oidc_id_token_lifetime, 0))
    end
  end

  @doc """
  Returns `true` if an ID token is to be issued on token renewal, `false` otherwise

  An ID token is always issued on initial request, hence that function deals only with renewal.

  ## Processing rules
  - If the client has the following field set to `true` for the corresponding flow and
  grant type, returns `true`:
    - `"__asteroid_oidc_flow_authorization_code_issue_id_token_refresh"`
    - `"__asteroid_oidc_flow_hybrid_issue_id_token_refresh"`
  - Otherwise, if the following configuration option is set to `true` for the corresponding flow
  and grant type, returns `true`:
    - #{Asteroid.Config.link_to_option(:oidc_flow_authorization_code_issue_id_token_refresh)}
    - #{Asteroid.Config.link_to_option(:oidc_flow_hybrid_issue_id_token_refresh)}
  - Otherwise uses the
  #{Asteroid.Config.link_to_option(:oidc_issue_id_token_refresh)} configuration option
  - Otherwise returns `false`
  """

  @spec issue_id_token?(Context.t()) :: boolean()

  def issue_id_token?(%{flow: flow, grant_type: :refresh_token, client: client}) do
    attr =
      case flow do
        :oidc_authorization_code ->
          "__asteroid_oidc_flow_authorization_code_issue_id_token_refresh"

        :oidc_hybrid ->
          "__asteroid_oidc_flow_hybrid_issue_id_token_refresh"
      end

    client = Client.fetch_attributes(client, [attr])

    if client.attrs[attr] == true do
      true
    else
      conf_opt =
        case flow do
          :oidc_authorization_code ->
            :oidc_flow_authorization_code_issue_id_token_refresh

          :oidc_hybrid ->
            :oidc_flow_hybrid_issue_id_token_refresh
        end

      astrenv(conf_opt, astrenv(:oidc_issue_id_token_refresh, false))
    end
  end

  def issue_id_token?(_) do
    false
  end

  @doc """
  Add subject claims from a list of claims
  """

  @spec add_sub_claims(t(), [String.t()], Subject.t()) :: t()

  def add_sub_claims(id_token, [], _subject) do
    id_token
  end

  def add_sub_claims(id_token, claims, subject) do
    claims_to_exclude = [
      "iss",
      "sub",
      "aud",
      "exp",
      "iat",
      "auth_time",
      "nonce",
      "acr",
      "amr",
      "azp"
    ]

    claims_to_load = Enum.filter(claims, &(&1 not in claims_to_exclude))

    subject = Subject.fetch_attributes(subject, claims_to_load)

    Enum.reduce(claims_to_load, id_token, &put_value(&2, &1, subject.attrs[&1]))
  end
end


<|start_filename|>lib/asteroid/oauth2/client.ex<|end_filename|>
defmodule Asteroid.OAuth2.Client do
  alias Asteroid.Client
  alias Asteroid.OAuth2
  alias OAuth2Utils.Scope

  import Asteroid.Utils

  @moduledoc """
  Util function to work with OAuth2 clients
  """

  defmodule AuthenticationError do
    @moduledoc """
    Error raised when an client authentication error occurs
    """

    defexception [:reason]

    @type t :: %__MODULE__{
            reason: :unknown_client | :unauthenticated_request
          }

    @impl true

    def message(%__MODULE__{reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "authentication error: #{String.replace(Atom.to_string(reason), "_", " ")}"

        :normal ->
          "authentication error: #{String.replace(Atom.to_string(reason), "_", " ")}"

        :minimal ->
          ""
      end
    end
  end

  defmodule AuthorizationError do
    @moduledoc """
    Error raised when an client is not authorized to perform an action
    """

    defexception [:reason]

    @type t :: %__MODULE__{
            reason: :unauthorized_grant_type | :unauthorized_scope
          }

    def message(%__MODULE__{reason: reason}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "The client is not authorized to perform this action (reason: #{inspect(reason)})"

        :normal ->
          "The client is not authorized to perform this action (reason: #{inspect(reason)})"

        :minimal ->
          ""
      end
    end
  end

  @typedoc """
  Client's type: confidential or public
  """

  @type type :: :confidential | :public

  @typedoc """
  String representation of a client type

  Must be the string conversion of a `t:type/0` atom.
  """

  @type type_str :: String.t()

  @doc """
  Returns the authenticated or **unauthenticated** client of a request

  To make sure that the client is authenticated, one shall use the `get_authenticated_client/1`
  function instead.
  """

  @spec get_client(Plug.Conn.t()) ::
          {:ok, Client.t()}
          | {:error, %AuthenticationError{}}

  def get_client(conn) do
    case get_authenticated_client(conn) do
      {:ok, client} ->
        {:ok, client}

      {:error, %AuthenticationError{reason: :unkown_client}} = error ->
        error

      {:error, _} ->
        get_unauthenticated_client(conn)
    end
  end

  @doc """
  Returns the APIac authenticated client, or an error if none was found
  """

  @spec get_authenticated_client(Plug.Conn.t()) ::
          {:ok, Client.t()}
          | {:error, %AuthenticationError{}}

  def get_authenticated_client(conn) do
    if APIac.authenticated?(conn) do
      case Client.load_from_unique_attribute("client_id", APIac.client(conn)) do
        {:ok, client} ->
          {:ok, client}

        {:error, _} ->
          {:error, AuthenticationError.exception(reason: :unkown_client)}
      end
    else
      {:error, AuthenticationError.exception(reason: :unauthenticated_request)}
    end
  end

  @doc """
  Returns the unauthenticated client of a request

  It does so by reading the `"client_id"` body parameter and trying to find the associated
  **public** client in the client's attribute repository. If it is found and it has no
  credentials (calling the `has_credentials?/1` function), it returns the client. Otherwise
  an error is returned.
  """

  @spec get_unauthenticated_client(Plug.Conn.t()) ::
          {:ok, Client.t()}
          | {:error, %AuthenticationError{}}

  def get_unauthenticated_client(conn) do
    case conn.body_params["client_id"] do
      nil ->
        {:error, AuthenticationError.exception(reason: :unauthenticated_request)}

      client_id ->
        if OAuth2Utils.valid_client_id_param?(client_id) do
          case Client.load_from_unique_attribute("client_id", client_id) do
            {:ok, client} ->
              if public?(client) do
                if not has_credentials?(client) do
                  {:ok, client}
                else
                  {:error,
                   AuthenticationError.exception(
                     reason: :public_client_has_credentials_and_must_authenticate
                   )}
                end
              else
                {:error, AuthenticationError.exception(reason: :unauthenticated_request)}
              end

            {:error, _} ->
              {:error, AuthenticationError.exception(reason: :unkown_client)}
          end
        else
          {:error,
           OAuth2.Request.MalformedParamError.exception(
             name: "client_id",
             value: client_id
           )}
        end
    end
  end

  @doc """
  Returns `true` if the client is allowed to use the grant type, `false` otherwise

  To be authorized to use a given grant type, the client's `"grant_types"` attribute
  must contain the given `t:Asteroid.OAuth2.grant_type_str/0`.
  """

  @spec grant_type_authorized?(Asteroid.Client.t(), Asteroid.OAuth2.grant_type_str()) ::
          :ok
          | {:error, %AuthorizationError{}}

  def grant_type_authorized?(client, grant_type) do
    client = Client.fetch_attributes(client, ["grant_types"])

    if grant_type in client.attrs["grant_types"] do
      :ok
    else
      {:error, AuthorizationError.exception(reason: :unauthorized_grant_type)}
    end
  end

  @doc """
  Returns `true` if the client is allowed to use the response type, `false` otherwise

  To be authorized to use a given grant type, the client's `"response_types"` attribute
  must contain the given `t:Asteroid.OAuth2.response_type_str/0`.
  """

  @spec response_type_authorized?(Asteroid.Client.t(), Asteroid.OAuth2.response_type_str()) ::
          :ok
          | {:error, %AuthorizationError{}}

  def response_type_authorized?(client, response_type) do
    client = Client.fetch_attributes(client, ["response_types"])

    if response_type in (client.attrs["response_types"] || []) do
      :ok
    else
      {:error, AuthorizationError.exception(reason: :unauthorized_response_type)}
    end
  end

  @doc """
  Returns `true` if the client is authorized to use the scopes, `false` otherwise

  Checks for each scope of the `Scope.Set.t()` if it's included in the  client's `"scope"`
  attribute.
  """

  @spec scopes_authorized?(Asteroid.Client.t(), Scope.Set.t()) ::
          :ok
          | {:error, %AuthorizationError{}}

  def scopes_authorized?(client, scope_set) do
    client = Client.fetch_attributes(client, ["scope"])

    if Scope.Set.subset?(scope_set, Scope.Set.new(client.attrs["scope"] || [])) do
      :ok
    else
      {:error, AuthorizationError.exception(reason: :unauthorized_scope)}
    end
  end

  @doc """
  Returns `true` if the client is a public client, `false` otherwise
  """

  @spec public?(Client.t()) :: boolean()
  def public?(client) do
    client = Client.fetch_attributes(client, ["client_type"])

    client.attrs["client_type"] == "public"
  end

  @doc """
  Returns `true` if the client is a confidential client, `false` otherwise
  """

  @spec confidential?(Client.t()) :: boolean()
  def confidential?(client) do
    client = Client.fetch_attributes(client, ["client_type"])

    client.attrs["client_type"] == "confidential"
  end

  @doc """
  Returns `true` if the client has credentials, `false` otherwise

  A client that has credentials is a client that has a `client_secret` attribute
  """

  @spec has_credentials?(Client.t()) :: boolean()

  def has_credentials?(client) do
    client = Client.fetch_attributes(client, ["client_secret"])

    client.attrs["client_secret"] != nil
  end

  @doc """
  Returns `:ok` is the client is authorized to introspect tokens on the `"/introspect"`
  endpoint, `{:error, :unauthorized}` otherwise

  An authorized client is a client that has been granted the use of the `"asteroid.introspect"`
  scope. See [Configuring clients - Asteroid scopes](configuring-clients.html#asteroid-scopes)
  for information on scopes.
  """

  @spec endpoint_introspect_authorized?(Client.t()) :: :ok | {:error, :unauthorized}

  def endpoint_introspect_authorized?(client) do
    client = Client.fetch_attributes(client, ["scope"])

    if "asteroid.introspect" in (client.attrs["scope"] || []) do
      :ok
    else
      {:error, :unauthorized}
    end
  end

  @doc """
  Returns `true` is the client must use PKCE, `false` otherwise

  A client must use PKCE when its
  `"__asteroid_oauth2_mandatory_pkce_use"` attribute is set to `true`.
  """

  @spec must_use_pkce?(Client.t()) :: boolean()

  def must_use_pkce?(client) do
    attribute = "__asteroid_oauth2_mandatory_pkce_use"

    client = Client.fetch_attributes(client, [attribute])

    client.attrs[attribute] == true
  end

  @doc """
  Returns the client secret from the client id of a client

  Can be used in as a callback in `APIacAuthBasic` and `APIacAuthClientSecretPost` in the
  configuration files:

  ```elixir
  {APIacAuthBasic,
    realm: "Asteroid",
    callback: &Asteroid.OAuth2.Client.get_client_secret/2,
    set_error_response: &APIacAuthBasic.save_authentication_failure_response/3,
    error_response_verbosity: :debug}
  ```
  """

  @spec get_client_secret(String.t(), String.t()) :: String.t()

  def get_client_secret(_realm, client_id) do
    case Client.load_from_unique_attribute("client_id", client_id, attributes: ["client_secret"]) do
      {:ok, client} ->
        client.attrs["client_secret"]

      {:error, _} ->
        nil
    end
  end
end


<|start_filename|>lib/asteroid/object_store/device_code.ex<|end_filename|>
defmodule Asteroid.ObjectStore.DeviceCode do
  @moduledoc """
  Behaviour for device code store
  """

  @type opts :: Keyword.t()

  @doc """
  Installs the device code store
  """

  @callback install(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the device code store (non supervised)
  """

  @callback start(opts()) :: :ok | {:error, any()}

  @doc """
  Starts the device code store (supervised)
  """

  @callback start_link(opts()) :: Supervisor.on_start()

  @doc """
  Returns an device code from its id

  Returns `{:ok, %Asteroid.Token.DeviceCode{}}` if the device code exists and `{:ok, nil}`
  otherwise.
  """

  @callback get(Asteroid.OAuth2.DeviceAuthorization.device_code(), opts()) ::
              {:ok, Asteroid.Token.DeviceCode.t() | nil}
              | {:error, any()}

  @doc """
  Returns the *device code id* from a user code
  """

  @callback get_from_user_code(Asteroid.OAuth2.DeviceAuthorization.user_code(), opts()) ::
              {:ok, Asteroid.Token.DeviceCode.t()} | {:error, any()}

  @doc """
  Stores an device code

  If the device code already exists, all of its data will be erased.
  """

  @callback put(Asteroid.Token.DeviceCode.t(), opts()) :: :ok | {:error, any()}

  @doc """
  Removes an device code
  """

  @callback delete(Asteroid.OAuth2.DeviceAuthorization.device_code(), opts()) ::
              :ok
              | {:error, any()}

  @optional_callbacks start: 1,
                      start_link: 1
end


<|start_filename|>lib/asteroid_web/error.ex<|end_filename|>
defmodule AsteroidWeb.Error do
  @moduledoc """
  Helper module to work with errors generated in the OAuth2 protocol flows
  """

  import Asteroid.Utils
  import Phoenix.Controller
  import Plug.Conn

  alias Asteroid.Client
  alias Asteroid.OAuth2
  alias Asteroid.OIDC
  alias AsteroidWeb.AuthorizeController
  alias AsteroidWeb.API.OAuth2.TokenController
  alias AsteroidWeb.API.OAuth2.RegisterController

  @doc """
  Responds with the appropriate error codes, headers and text to an OAuth2 protocol flow
  error
  """

  @spec respond_api(Plug.Conn.t(), Exception.t()) :: Plug.Conn.t()

  def respond_api(conn, %OAuth2.Client.AuthenticationError{} = e) do
    error_status = err_status(e)
    error_name = err_name(e)

    error_response =
      %{}
      |> Map.put("error", error_name)
      |> put_if_not_empty_string("error_description", Exception.message(e))

    conn
    |> put_status(error_status)
    |> set_www_authenticate_header()
    |> json(error_response)
  end

  def respond_api(conn, e) do
    error_status = err_status(e)
    error_name = err_name(e)

    error_response =
      %{}
      |> Map.put("error", error_name)
      |> put_if_not_empty_string("error_description", Exception.message(e))

    conn
    |> put_status(error_status)
    |> json(error_response)
  end

  @spec respond_authorize(Plug.Conn.t(), Exception.t()) :: Plug.Conn.t()

  def respond_authorize(conn, %OAuth2.Request.MalformedParamError{name: "redirect_uri"} = e) do
    conn
    |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.AuthorizeView)
    |> render("error_redirect_uri.html")
  end

  def respond_authorize(conn, %OAuth2.Request.MalformedParamError{name: "client_id"} = e) do
    conn
    |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.AuthorizeView)
    |> render("error_redirect_uri.html")
  end

  def respond_authorize(conn, %OAuth2.Request.InvalidRequestError{parameter: "redirect_uri"} = e) do
    conn
    |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.AuthorizeView)
    |> render("error_redirect_uri.html")
  end

  def respond_authorize(conn, %OAuth2.Request.InvalidRequestError{parameter: "client_id"} = e) do
    conn
    |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.AuthorizeView)
    |> render("error_redirect_uri.html")
  end

  # function used when the connection is directly returned with an error. In such a case, params
  # are in the Conn

  def respond_authorize(
        %Plug.Conn{query_params: %{"redirect_uri" => redirect_uri, "client_id" => client_id}} =
          conn,
        e
      ) do
    with :ok <- AuthorizeController.client_id_valid?(client_id),
         :ok <- AuthorizeController.redirect_uri_valid?(redirect_uri),
         {:ok, client} <- Client.load_from_unique_attribute("client_id", client_id),
         :ok <-
           AuthorizeController.redirect_uri_registered_for_client?(
             client,
             redirect_uri
           ) do
      redirect_uri =
        OAuth2.RedirectUri.add_params(
          redirect_uri,
          %{}
          |> Map.put("error", err_name(e))
          |> Map.put("error_description", Exception.message(e))
          |> put_if_not_nil("state", conn.query_params["state"])
        )

      conn
      |> redirect(external: redirect_uri)
    else
      _ ->
        conn
        |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
        |> put_status(400)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.AuthorizeView)
        |> render("error_redirect_uri.html")
    end
  end

  # function used when coming back from an authn / authz process. The authorization request must
  # be put in the assigns under the :authz_request atom

  def respond_authorize(%Plug.Conn{assigns: %{:authz_request => authz_request}} = conn, e) do
    redirect_uri =
      OAuth2.RedirectUri.add_params(
        authz_request.redirect_uri,
        %{"error" => err_name(e)}
        |> put_if_not_empty_string("error_description", Exception.message(e))
        |> put_if_not_nil("state", authz_request.params["state"])
      )

    conn
    |> redirect(external: redirect_uri)
  end

  def respond_authorize(conn, %OAuth2.JAR.RequestNotSupportedError{} = e) do
    respond_authorize_jar(conn, e)
  end

  def respond_authorize(conn, %OAuth2.JAR.InvalidRequestObjectError{} = e) do
    respond_authorize_jar(conn, e)
  end

  # Here we certainly won't have a redirect URI since we couldn't didn/t resolve the request
  # object in the first place

  def respond_authorize(conn, %OAuth2.JAR.RequestURINotSupportedError{} = e) do
    conn
    |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.AuthorizeView)
    |> render("error_redirect_uri.html")
  end

  def respond_authorize(conn, %OAuth2.JAR.InvalidRequestURIError{} = e) do
    conn
    |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
    |> put_status(400)
    |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
    |> put_view(AsteroidWeb.AuthorizeView)
    |> render("error_redirect_uri.html")
  end

  # JAR request which do not have duplicate query params for client_id or redirect_uri
  # (if they do, another respond_authorize/2 clause will have caught it)
  #
  # We then try to read the request object anyway (obvisouly won't work for JWEs) to pick
  # client_id / redirect_uri, then validate them and if correct, redirect to the client.
  # Otherwise we show a local Asteroid error page

  @spec respond_authorize_jar(Plug.Conn.t(), Exception.t()) :: Plug.Conn.t()

  defp respond_authorize_jar(conn, e) do
    try do
      invalid_request_object =
        e.request_object
        |> JOSE.JWS.peek_payload()
        |> Jason.decode!()

      case invalid_request_object do
        %{"redirect_uri" => redirect_uri, "client_id" => client_id} ->
          with :ok <- AuthorizeController.client_id_valid?(client_id),
               :ok <- AuthorizeController.redirect_uri_valid?(redirect_uri),
               {:ok, client} <- Client.load_from_unique_attribute("client_id", client_id),
               :ok <-
                 AuthorizeController.redirect_uri_registered_for_client?(
                   client,
                   redirect_uri
                 ) do
            redirect_uri =
              OAuth2.RedirectUri.add_params(
                redirect_uri,
                %{}
                |> Map.put("error", err_name(e))
                |> Map.put("error_description", Exception.message(e))
                |> put_if_not_nil("state", conn.query_params["state"])
              )

            conn
            |> redirect(external: redirect_uri)
          else
            _ ->
              conn
              |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
              |> put_status(400)
              |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
              |> put_view(AsteroidWeb.AuthorizeView)
              |> render("error_redirect_uri.html")
          end

        _ ->
          conn
          |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
          |> put_status(400)
          |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
          |> put_view(AsteroidWeb.AuthorizeView)
          |> render("error_redirect_uri.html")
      end
    rescue
      _ ->
        conn
        |> put_flash(:error, "An error has occured (#{Exception.message(e)})")
        |> put_status(400)
        |> Phoenix.Controller.put_layout({AsteroidWeb.LayoutView, "app.html"})
        |> put_view(AsteroidWeb.AuthorizeView)
        |> render("error_redirect_uri.html")
    end
  end

  @spec err_name(Exception.t()) :: String.t()

  defp err_name(%OAuth2.UnsupportedGrantTypeError{}), do: "unsupported_grant_type"
  defp err_name(%OAuth2.UnsupportedResponseTypeError{}), do: "unsupported_response_type"
  defp err_name(%OAuth2.InvalidGrantError{}), do: "invalid_grant"
  defp err_name(%OAuth2.AccessDeniedError{}), do: "access_denied"
  defp err_name(%OAuth2.ServerError{}), do: "server_error"
  defp err_name(%OAuth2.TemporarilyUnavailableError{}), do: "temporarily_unavailable"
  defp err_name(%OIDC.InteractionRequiredError{}), do: "interaction_required"
  defp err_name(%OIDC.LoginRequiredError{}), do: "login_required"
  defp err_name(%OIDC.AccountSelectionRequiredError{}), do: "account_selection_required"
  defp err_name(%OIDC.ConsentRequiredError{}), do: "consent_required"
  defp err_name(%OAuth2.Client.AuthenticationError{}), do: "invalid_client"

  defp err_name(%OAuth2.Client.AuthorizationError{reason: :unauthorized_scope}),
    do: "invalid_scope"

  defp err_name(%OAuth2.Client.AuthorizationError{}), do: "unauthorized_client"
  defp err_name(%OAuth2.Request.InvalidRequestError{}), do: "invalid_request"
  defp err_name(%OAuth2.Request.MalformedParamError{name: "scope"}), do: "invalid_scope"
  defp err_name(%OAuth2.Request.MalformedParamError{}), do: "invalid_request"
  defp err_name(%OAuth2.Scope.UnknownRequestedScopeError{}), do: "invalid_scope"
  defp err_name(%OAuth2.DeviceAuthorization.ExpiredTokenError{}), do: "expired_token"

  defp err_name(%OAuth2.DeviceAuthorization.AuthorizationPendingError{}),
    do: "authorization_pending"

  defp err_name(%OAuth2.DeviceAuthorization.RateLimitedError{}), do: "slow_down"
  defp err_name(%OAuth2.JAR.RequestNotSupportedError{}), do: "request_not_supported"
  defp err_name(%OAuth2.JAR.RequestURINotSupportedError{}), do: "request_uri_not_supported"
  defp err_name(%OAuth2.JAR.InvalidRequestURIError{}), do: "invalid_request_uri"
  defp err_name(%OAuth2.JAR.InvalidRequestObjectError{}), do: "invalid_request_object"
  defp err_name(%TokenController.ExceedingScopeError{}), do: "invalid_scope"

  defp err_name(%RegisterController.InvalidClientMetadataFieldError{}),
    do: "invalid_client_metadata"

  defp err_name(%RegisterController.InvalidRedirectURIError{}), do: "invalid_redirect_uri"

  defp err_name(%RegisterController.UnauthorizedRequestedScopesError{}),
    do: "invalid_client_metadata"

  @spec err_status(Exception.t()) :: non_neg_integer()

  defp err_status(%OAuth2.Client.AuthenticationError{}), do: 401
  defp err_status(_), do: 400

  @doc """
  Set the authentication error headers in accordance to RFC6749

  This function set the `"www-authenticate"` header in the following way:
  - if an authentication was attempted, sets the header for this authentication attempt only
  - otherwise, advertise all the authentication methods available

  As per the RFC:

  > If the
  > client attempted to authenticate via the "Authorization"
  > request header field, the authorization server MUST
  > respond with an HTTP 401 (Unauthorized) status code and
  > include the "WWW-Authenticate" response header field
  > matching the authentication scheme used by the client.
  """

  @spec set_www_authenticate_header(Plug.Conn.t()) :: Plug.Conn.t()

  def set_www_authenticate_header(conn) do
    apisex_errors = APIac.AuthFailureResponseData.get(conn)

    failed_auth =
      Enum.find(
        apisex_errors,
        fn apisex_error ->
          apisex_error.reason != :credentials_not_found and
            is_tuple(apisex_error.www_authenticate_header)
        end
      )

    case failed_auth do
      %APIac.AuthFailureResponseData{www_authenticate_header: {scheme, params}} ->
        APIac.set_WWWauthenticate_challenge(conn, scheme, params)

      # no failed authn at all or one that can return www-authenticate header
      nil ->
        Enum.reduce(
          apisex_errors,
          conn,
          fn
            %APIac.AuthFailureResponseData{www_authenticate_header: {scheme, params}}, conn ->
              APIac.set_WWWauthenticate_challenge(conn, scheme, params)

            _, conn ->
              conn
          end
        )
    end
  end
end


<|start_filename|>lib/asteroid/client.ex<|end_filename|>
defmodule Asteroid.Client do
  use AttributeRepository.Resource, otp_app: :asteroid

  @moduledoc """
  `AttributeRepository.Resource` for clients

  Client refers to an OAuth2 client, that is a **application** (and not a machine). There are 2
  types of clients:
  - those who can keep a secret secret: *confidential clients* (such as a server). Usually there
  is one instance of this application running (even though it has several servers running), so in
  this case 1 client = 1 machine
  - those who can't: *public clients* (mobile applications, SPAs...). In this case there are
  multiple instances of the same client running, used by different subjects

  ## Field naming
  The following fields have standardised meaning:
  - `"client_id"`: the client identifier (as in OAuth2) (`String.t()`)
  - `"client_name"`: a map whose keys are the language code and the values the localized names
  - `"client_uri"`: a map whose keys are the language code and the values the localized URIs
  - `"logo_uri"`: a map whose keys are the language code and the values the localized logos
  - `"tos_uri"`: a map whose keys are the language code and the values the localized TOS URIs
  - `"policy_uri"`: a map whose keys are the language code and the values the localized policy
  URIs
  - `"client_secret"`: the client secret (`String.t()`). Can be the plain secret, or the
  `t:Expwd.Hashed.Portable.t/0`
  - `"client_type"`: `"public"` or `"confidential"`, depending on the client's type
  - `"grant_types"`: the list of grant types (`t:Asteroid.OAuth2.grant_type_str/0`) that the
  client is allowed to use
  - `"response_types"`: the list of response types (`t:Asteroid.OAuth2.response_type_str/0`) that
  the client is allowed to use
  - `"redirect_uris"`: the list of OAuth2 / OpenID Connect redirect URIs (`[String.t()]`)
  - `"scope"`: a list of OAuth2 scopes that the client can use when requesting tokens. Scopes
  starting with the string `"asteroid."` are special permissions used to access Asteroid
  endpoints. See also []()
  - `"token_endpoint_auth_method"`: a `t:Asteroid.Oauth2.Endpoint.auth_method_str/0`
  as specified in RFC7591
  - `"contacts"`: list of email addresses (`[String.t()]`)
  - `"jwks_uri"`: the JWKs URI of the client (`String.t()`)
  - `"jwks"`: the JWKs of the client (a list of JWKs)
  - `"software_id"`: the software ID (`String.t()`). Not used by Asteroid
  - `"software_version"`: the software version (`String.t()`). Not used by Asteroid
  - `"application_type"`: the application type (either `nil`, `"web"` or `"native"`)
  - `"sector_identifier_uri"`: the sector identifier URI (`String.t()`)
  - `"subject_type"`: the subject type (either `nil`, `"public"` or `"pairwise"`))
  - `"id_token_signed_response_alg"`: refer to the OpenID Connect Client Registration specification
  - `"id_token_encrypted_response_alg"`: refer to the OpenID Connect Client Registration specification
  - `"id_token_encrypted_response_enc"`: refer to the OpenID Connect Client Registration specification
  - `"userinfo_signed_response_alg"`: refer to the OpenID Connect Client Registration specification
  - `"userinfo_encrypted_response_alg"`: refer to the OpenID Connect Client Registration specification
  - `"userinfo_encrypted_response_enc"`: refer to the OpenID Connect Client Registration specification
  - `"request_object_signed_response_alg"`: refer to the OpenID Connect Client Registration specification
  - `"request_object_encrypted_response_alg"`: refer to the OpenID Connect Client Registration specification
  - `"request_object_encrypted_response_enc"`: refer to the OpenID Connect Client Registration specification
  - `"default_max_age"`: refer to the OpenID Connect Client Registration specification
  - `"require_auth_time"`: refer to the OpenID Connect Client Registration specification
  - `"default_acr_values"`: refer to the OpenID Connect Client Registration specification
  - `"__asteroid_created_by_client_id"`: the `String.t()` client id of the client that has
  initially created this client using the `/register` endpoint (may not have a value if the
  client was created by another mean)
  - `"__asteroid_oauth2_flow_ropc_issue_refresh_token_init"`: a `boolean()` set to true if a
  refresh token is to be issued at the first request of the ROPC flow
  - `"__asteroid_oauth2_flow_ropc_issue_refresh_token_refresh"`: a `boolean()` set to true if a
  refresh token is to be issued when refresh tokens in the ROPC flow
  - `"__asteroid_oauth2_flow_ropc_refresh_token_lifetime"`: a `non_neg_integer()` set to the
  lifetime duration of a refresh token in the ROPC flow
  - `"__asteroid_oauth2_flow_ropc_access_token_lifetime"`: a `non_neg_integer()` set to the
  lifetime duration of an access token in the ROPC flow
  - `"__asteroid_oauth2_flow_ropc_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the ROPC flow
  - `"__asteroid_oauth2_flow_ropc_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the ROPC flow
  - `"__asteroid_oauth2_flow_ropc_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the ROPC flow
  - `"__asteroid_oauth2_flow_client_credentials_issue_refresh_token_init"`: a `boolean()` set to
  `true` if a refresh token is to be issued at the first request of the client credentials flow
  - `"__asteroid_oauth2_flow_client_credentials_issue_refresh_token_refresh"`: a `boolean()` set
  to `true` if a refresh token is to be issued when refresh tokens in the client_credentials flow
  - `"__asteroid_oauth2_flow_client_credentials_refresh_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of a refresh token in the client credentials flow
  - `"__asteroid_oauth2_flow_client_credentials_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the client credentials flow
  - `"__asteroid_oauth2_flow_client_credentials_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the client credentials
  flow
  - `"__asteroid_oauth2_flow_client_credentials_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the client credentials flow
  - `"__asteroid_oauth2_flow_client_credentials_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the client credentials
  flow
  - `"__asteroid_oauth2_flow_authorization_code_authorization_code_lifetime"`: a
  `non_neg_integer()` set to the lifetime duration of an authorization in the code flow
  - `"__asteroid_oauth2_flow_authorization_code_issue_refresh_token_init"`: a `boolean()` set to
  true if a refresh token is to be issued in the authorization code flow when presenting the
  authorization code
  - `"__asteroid_oauth2_flow_authorization_code_issue_refresh_token_refresh"`: a `boolean()` set
  to true if a refresh token is to be issued when refreshing tokens in the authorization code flow
  - `"__asteroid_oauth2_flow_authorization_code_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the authorization code flow
  - `"__asteroid_oauth2_flow_authorization_code_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the authorization code
  flow
  - `"__asteroid_oauth2_flow_authorization_code_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the authorization code flow
  - `"__asteroid_oauth2_flow_authorization_code_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the authorization code
  flow
  - `"__asteroid_oauth2_flow_authorization_code_refresh_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of a refresh token in the authorization code flow
  - `"__asteroid_oauth2_flow_implicit_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the implicit flow
  - `"__asteroid_oauth2_flow_implicit_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the implicit flow
  - `"__asteroid_oauth2_flow_implicit_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the implicit flow
  - `"__asteroid_oauth2_flow_implicit_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the implicit flow
  - `"__asteroid_endpoint_introspect_claims_resp"`: the list of `String.t()` claims to be
  returned from the `"/introspect"` endpoint
  - `"__asteroid_oauth2_mandatory_pkce_use"`: a `boolean()` indicating
  whether the client shall use PKCE or not (defaults to not being forced to use PKCE)
  - `"__asteroid_oauth2_endpoint_register_allowed_token_endpoint_auth_method"`: a list of
  `t:Asteroid.OAuth2.Endpoint.auth_method_str/0` that restricts the token endpoint auth methods
  that can be assigned to a new client created by this client. If absent or set to `nil`, all
  supported methods
  (#{Asteroid.Config.link_to_option(:oauth2_endpoint_token_auth_methods_supported_callback)})
  can be assigned to new clients
  - `"__asteroid_oauth2_endpoint_register_allowed_grant_types"`: a list of
  `t:Asteroid.OAuth2.grant_type_str/0` of grant types that can be assigned on newly created
  clients on the client registration endpoint. This is opt-in: when not set to a client,
  it will not be capable of creating new clients
  - `"__asteroid_oauth2_endpoint_register_allowed_response_types"`: a list of
  `t:Asteroid.OAuth2.response_type_str/0` of response types that can be assigned to newly created
  clients on the client registration endpoint. This is opt-in: when not set to a client,
  it will not be capable of creating new clients
  - `"__asteroid_oauth2_endpoint_register_allowed_scopes"`: a list of scopes that can be
  assigned to newly created clients on the client registration endpoint. If not set, defaults
  to the available scopes for the granted flows (determined from the grant types)
  - `"__asteroid_oauth2_endpoint_register_auto_scopes"`: a list of scopes that are automatically
  assigned to newly created clients, in addition to those requested. The existence of these
  automatically granted scopes are *not checked* against the configured scopes, which means
  that scopes that are not configured in the configuration files can be granted through this
  option
  - `"__asteroid_oauth2_endpoint_register_additional_metadata_fields"`: a list of strings
  for the additional metadata fields that will be saved upon client creation request
  - `"__asteroid_oauth2_endpoint_register_default_token_endpoint_auth_method"`: a
  `t:Asteroid.OAuth2.Endpoint.auth_method_str/0` that replaces the specification's default
  (`"client_secret_basic"`) for new clients created by this client
  - `"__asteroid_oauth2_endpoint_register_default_grant_types"`: a list of
  `t:Asteroid.OAuth2.grant_type_str/0` that replaces the specification's default
  (`["authorization_code"]`) for new clients created by this client
  - `"__asteroid_oauth2_endpoint_register_default_response_types"`: a list of
  `t:Asteroid.OAuth2.response_type_str/0` that replaces the specification's default
  (`["code"]`) for new clients created by this client
  - `"__asteroid_oauth2_flow_device_authorization_device_code_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of a device code in the device authorization flow
  - `"__asteroid_oauth2_flow_device_authorization_issue_refresh_token_init"`: a `boolean()` set
  to true if a refresh token is to be issued at the first request of the device authorization
  flow
  - `"__asteroid_oauth2_flow_device_authorization_issue_refresh_token_refresh"`: a `boolean()`
  set to true if a refresh token is to be issued when refresh tokens in the device authorization
  flow
  - `"__asteroid_oauth2_flow_device_authorization_refresh_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of a refresh token in the device authorization flow
  - `"__asteroid_oauth2_flow_device_authorization_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the device authorization flow
  - `"__asteroid_oauth2_flow_device_authorization_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the device authorization
  flow
  - `"__asteroid_oauth2_flow_device_authorization_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the device authorization
  flow
  - `"__asteroid_oauth2_flow_device_authorization_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the device
  authorization flow
  - `"__asteroid_oidc_flow_authorization_code_authorization_code_lifetime"`: a
  `non_neg_integer()` set to the lifetime duration of an authorization code in the OIDC code flow
  - `"__asteroid_oidc_flow_authorization_code_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the OIDC authorization code flow
  - `"__asteroid_oidc_flow_authorization_code_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the OIDC authorization
  code flow
  - `"__asteroid_oidc_flow_authorization_code_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the OIDC authorization
  code flow
  - `"__asteroid_oidc_flow_authorization_code_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the OIDC authorization
  code flow
  - `"__asteroid_oidc_flow_authorization_code_issue_refresh_token_init"`: a `boolean()` set to
  true if a refresh token is to be issued in the OIDC authorization code flow when presenting the
  authorization code
  - `"__asteroid_oidc_flow_authorization_code_issue_refresh_token_refresh"`: a `boolean()` set
  to true if a refresh token is to be issued when refreshing tokens in the OIDC authorization
  code flow
  - `"__asteroid_oidc_flow_hybrid_issue_refresh_token_init"`: a `boolean()` set to
  true if a refresh token is to be issued in the OIDC hybrid flow when presenting the
  authorization code
  - `"__asteroid_oidc_flow_hybrid_issue_refresh_token_refresh"`: a `boolean()` set
  to true if a refresh token is to be issued when refreshing tokens in the OIDC hybrid flow
  - `"__asteroid_oidc_flow_authorization_code_refresh_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of a refresh token in the OIDC authorization code flow
  - `"__asteroid_oidc_flow_hybrid_refresh_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of a refresh token in the OIDC hybrid flow
  - `"__asteroid_oidc_flow_authorization_code_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the OIDC authorization code flow
  - `"__asteroid_oidc_flow_implicit_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the OIDC implicit flow
  - `"__asteroid_oidc_flow_hybrid_access_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an access token in the OIDC hybrid flow
  - `"__asteroid_oidc_flow_authorization_code_id_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an ID token in the OIDC authorization code flow
  - `"__asteroid_oidc_flow_implicit_id_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an ID token in the OIDC implicit flow
  - `"__asteroid_oidc_flow_hybrid_id_token_lifetime"`: a `non_neg_integer()`
  set to the lifetime duration of an ID token in the OIDC hybrid flow
  - `"__asteroid_oidc_flow_authorization_code_issue_id_token_refresh"`: a `boolean()` set
  to true if an ID token is to be issued when refreshing tokens in the OIDC authorization code
  flow
  - `"__asteroid_oidc_flow_hybrid_issue_id_token_refresh"`: a `boolean()` set
  to true if an ID token is to be issued when refreshing tokens in the OIDC hybrid flow
  - `"__asteroid_oidc_flow_hybrid_authorization_code_lifetime"`: a
  `non_neg_integer()` set to the lifetime duration of an authorization code in the OIDC code flow
  - `"__asteroid_oidc_flow_implicit_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the OIDC implicit flow
  - `"__asteroid_oidc_flow_hybrid_access_token_serialization_format"`: the
  `t:Asteroid.Token.serialization_format_str/0` serialization format for the OIDC hybrid flow
  - `"__asteroid_oidc_flow_implicit_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the OIDC implicit flow
  - `"__asteroid_oidc_flow_hybrid_access_token_signing_key"`: the
  `t:Asteroid.Crypto.Key.name/0` signing key name for access tokens in the OIDC hybrid flow
  - `"__asteroid_oidc_flow_implicit_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the OIDC implicit flow
  - `"__asteroid_oidc_flow_hybrid_access_token_signing_alg"`: the
  `t:Asteroid.Crypto.Key.jws_alg/0` signing algorithm for access tokens in the OIDC hybrid flow

  ## Configuration

  This modules uses the default configuration of `AttributeRepository.Resource` (see `config/1`).

  ## Security considerations

  - When using client secrets, make sure to 1) generate their secrets randomly and 2) store them
  in an appropriate manner. See the [expwd](https://github.com/tanguilp/expwd) library.
  - You **SHOULD NOT** issue client secrets to public clients
  [RFC6749 - 10.1.  Client Authentication](https://tools.ietf.org/html/rfc6749#section-10.1)
  """

  @doc """
  Returns the JWKs of a client

  Note that the `"jwks_uri"` field takes precedence over the `"jwks"` field. If `"jwks"` is
  somehow unreachable, it does **not** fallback to the `"jwks"` field but returns an error
  instead.
  """

  @spec get_jwks(t()) :: {:ok, [Asteroid.Crypto.Key.t()]} | {:error, any()}

  def get_jwks(client) do
    client = fetch_attributes(client, ["jwks", "jwks_uri"])

    if client.attrs["jwks_uri"] do
      case JWKSURIUpdater.get_keys(client.attrs["jwks_uri"]) do
        {:ok, keys} ->
          keys

        {:error, _} = error ->
          error
      end
    else
      {:ok, client.attrs["jwks"] || []}
    end
  end
end


<|start_filename|>lib/asteroid/context.ex<|end_filename|>
defmodule Asteroid.Context do
  @moduledoc """
  OAuth2 and connection context passed to callback functions

  This is basically a map containing:
  - the current endpoint
  - the current flow
  - the used grant type
  - the requested scopes
  - the scopes released after calling the relevant callbacks
  - the client object
  - the subject object
  - the device object
  - and any other useful information

  Note that for pattern matching convenience, the define fields cannot be `nil`. For example,
  there will never be a `:subject` field in client credentials flow.
  """

  alias OAuth2Utils.Scope

  @type t :: %{
          optional(:endpoint) => Asteroid.OAuth2.endpoint() | Asteroid.OIDC.endpoint(),
          optional(:flow) => Asteroid.OAuth2.flow(),
          optional(:grant_type) => Asteroid.OAuth2.grant_type(),
          optional(:response_type) => Asteroid.OAuth2.response_type(),
          optional(:requested_scopes) => Scope.Set.t(),
          optional(:granted_scopes) => Scope.Set.t(),
          optional(:client) => Asteroid.Client.t(),
          optional(:subject) => Asteroid.Subject.t(),
          optional(:device) => Asteroid.Device.t(),
          optional(:conn) => Plug.Conn.t(),
          optional(any()) => any()
        }
end


<|start_filename|>lib/asteroid/oauth2/pkce.ex<|end_filename|>
defmodule Asteroid.OAuth2.PKCE do
  @moduledoc false

  alias Asteroid.OAuth2

  @type code_challenge :: String.t()

  @type code_challenge_method :: :plain | :S256

  @typedoc """
  Must be the string representation of `t:code_challenge_method/0`
  """

  @type code_challenge_method_str :: String.t()

  @type code_verifier :: String.t()

  defmodule MalformedCodeChallengeError do
    @moduledoc """
    Exception returned when a code challenge is malformed

    Note that the length is restricted: from 43 to 128 characters.
    """

    defexception [:code_challenge]

    @impl true

    def message(%{code_challenge: code_challenge}) when byte_size(code_challenge) < 43 do
      "Code challenge must be at least 43 characters"
    end

    def message(%{code_challenge: code_challenge}) when byte_size(code_challenge) > 128 do
      "Code challenge must be no more than 128 characters"
    end

    def message(%{code_challenge: code_challenge}) do
      "Invalid character in code challenge `#{code_challenge}`"
    end
  end

  defmodule UnsupportedCodeChallengeMethodError do
    @moduledoc """
    Exception returned when a code challenge method is not supported

    Supported methods are those of `t:code_challenge_method/0` and are activated with the
    #{Asteroid.Config.link_to_option(:oauth2_pkce_allowed_methods)}
    configuration option.
    """

    defexception [:code_challenge_method_str]

    @impl true

    def message(%{code_challenge_method_str: code_challenge_method_str}) do
      "Invalid code challenge `#{code_challenge_method_str}`"
    end
  end

  @doc """
  Returns `:ok` if the code challenge is valid,
  `{:error, %Asteroid.OAuth2.PKCE.InvalidCodeChallengeError{}}` otherwise
  """

  @spec code_challenge_valid?(code_challenge()) :: boolean()

  def code_challenge_valid?(code_challenge) do
    Regex.run(~r<^[\x41-\x5A\x61-\x7A\x30-\x39._~-]{43,128}$>, code_challenge) != nil
  end

  @doc """
  Returns `t:code_challenge_method/0` if the parameter is a valid code challenge method,
  `nil` otherwise
  """

  @spec code_challenge_method_from_string(String.t()) :: atom() | nil

  def code_challenge_method_from_string("plain"), do: :plain
  def code_challenge_method_from_string("S256"), do: :S256
  def code_challenge_method_from_string(_), do: nil

  @doc """
  Returns `:ok` if the code verifier is validated against the code challenge,
  `{:error, %Asteroid.OAuth2.PKCE.InvalidCodeVerifierError{}}` otherwise
  """

  @spec verify_code_verifier(code_verifier(), code_challenge(), code_challenge_method()) ::
          :ok
          | {:error, %OAuth2.InvalidGrantError{}}

  def verify_code_verifier(code_verifier, code_challenge, :plain) do
    if code_verifier == code_challenge do
      :ok
    else
      {:error,
       OAuth2.InvalidGrantError.exception(
         grant: "code_verifier",
         reason: "invalid code verifier",
         debug_details: "code_verifier: `#{code_verifier}, code_challenge_method: `:plain`)}"
       )}
    end
  end

  def verify_code_verifier(code_verifier, code_challenge, :S256) do
    if Base.url_encode64(:crypto.hash(:sha256, code_verifier), padding: false) == code_challenge do
      :ok
    else
      {:error,
       OAuth2.InvalidGrantError.exception(
         grant: "code_verifier",
         reason: "invalid code verifier",
         debug_details: "code_verifier: `#{code_verifier}, code_challenge_method: `:S256`)}"
       )}
    end
  end
end


<|start_filename|>lib/asteroid/attribute_repository.ex<|end_filename|>
defmodule Asteroid.AttributeRepository do
  import Asteroid.Utils

  require Logger

  @spec auto_install_from_config() :: :ok | {:error, any()}

  def auto_install_from_config() do
    conf_list = astrenv(:attribute_repositories)

    do_auto_install_from_config(conf_list)
  end

  @spec do_auto_install_from_config([Keyword.t()]) :: :ok | {:error, any()}

  defp do_auto_install_from_config([{rep, conf} | t]) do
    if conf[:auto_install] != false do
      Logger.info("#{__MODULE__}: configuring attribute repository `#{rep}`")

      case conf[:module].install(conf[:run_opts] || [], conf[:init_opts] || []) do
        :ok ->
          do_auto_install_from_config(t)

        {:error, _} = error ->
          error
      end
    else
      do_auto_install_from_config(t)
    end
  end

  defp do_auto_install_from_config([]) do
    :ok
  end

  @spec auto_start_from_config() :: :ok | {:error, any()}

  def auto_start_from_config() do
    conf_list = astrenv(:attribute_repositories)

    do_auto_start_from_config(conf_list)
  end

  @spec do_auto_start_from_config([Keyword.t()]) :: :ok | {:error, any()}

  defp do_auto_start_from_config([{rep, conf} | t]) do
    if conf[:auto_start] != false do
      Logger.info("#{__MODULE__}: starting attribute repository `#{rep}`")

      impl = conf[:module]

      if {:start_link, 1} in impl.__info__(:functions) do
        case impl.start_link(conf[:init_opts] || []) do
          {:ok, _pid} ->
            do_auto_start_from_config(t)

          {:error, _} = error ->
            error
        end
      else
        case impl.start(conf[:init_opts] || []) do
          :ok ->
            do_auto_start_from_config(t)

          {:error, _} = error ->
            error
        end
      end
    else
      do_auto_start_from_config(t)
    end
  end

  defp do_auto_start_from_config([]) do
    :ok
  end
end


<|start_filename|>test/asteroid_web/controllers/api/oauth2/introspect_controller_test.exs<|end_filename|>
defmodule AsteroidWeb.API.OAuth2.IntrospectControllerTest do
  use AsteroidWeb.ConnCase, async: true

  import Asteroid.Utils

  alias Asteroid.Token.{RefreshToken, AccessToken}
  alias AsteroidWeb.Router.Helpers, as: Routes
  alias OAuth2Utils.Scope

  ##########################################################################
  # General tests
  ##########################################################################

  test "invalid content-type", %{conn: conn} do
    assert_raise Plug.Parsers.UnsupportedMediaTypeError, fn ->
      conn
      |> put_req_header("content-type", "plain/text")
      |> post(Routes.introspect_path(conn, :handle), "Some plain text")
      |> json_response(400)
    end
  end

  test "missing token parameter", %{conn: conn} do
    req_body = %{
      "other" => "parameter"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "invalid token parameter", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "invalid token_type_hint parameter", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>",
      "token_type_hint" => "unknown"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(400)

    assert response["error"] == "invalid_request"
  end

  test "no credentials for confidential client", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    conn = post(conn, Routes.introspect_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  # next 2 tests testing that:
  #
  # If the
  # client attempted to authenticate via the "Authorization"
  # request header field, the authorization server MUST
  # respond with an HTTP 401 (Unauthorized) status code and
  # include the "WWW-Authenticate" response header field
  # matching the authentication scheme used by the client.
  #
  # from https://tools.ietf.org/html/rfc6749#section-5.2
  test "invalid basic credentials for confidential client", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    conn =
      conn
      |> put_req_header("authorization", basic_auth_header("invalid_client", "secret"))
      |> post(Routes.introspect_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm=)

    refute Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer )

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  test "public client with no credentials forbidden", %{conn: conn} do
    req_body = %{
      "token" => "<PASSWORD>"
    }

    conn = post(conn, Routes.introspect_path(conn, :handle), req_body)

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="always erroneous client password")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Basic realm="Asteroid")

    assert Plug.Conn.get_resp_header(conn, "www-authenticate") |> List.first() =~
             ~s(Bearer realm="Asteroid")

    response = json_response(conn, 401)

    assert response["error"] == "invalid_client"
  end

  ##########################################################################
  # Endpoint test - access tokens
  ##########################################################################

  test "existing access token, no token_type_hint param, basic auth", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == access_token.data["iss"]
    assert response["client_id"] == access_token.data["client_id"]
    assert response["token_type"] == access_token.data["token_type"]
    assert response["exp"] == access_token.data["exp"]
    assert response["iat"] == access_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(access_token.data["scope"])
           )
  end

  test "existing access token, no token_type_hint param, basic auth with no scopes", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", [])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == access_token.data["iss"]
    assert response["client_id"] == access_token.data["client_id"]
    assert response["token_type"] == access_token.data["token_type"]
    assert response["exp"] == access_token.data["exp"]
    assert response["iat"] == access_token.data["iat"]
    assert response["scope"] == nil
  end

  test "existing access token, no token_type_hint param, bearer auth", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id
    }

    {:ok, auth_access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("scope", "asteroid.introspect")
      |> AccessToken.store()

    response =
      conn
      |> put_req_header("authorization", "Bearer " <> AccessToken.serialize(auth_access_token))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == access_token.data["iss"]
    assert response["client_id"] == access_token.data["client_id"]
    assert response["token_type"] == access_token.data["token_type"]
    assert response["exp"] == access_token.data["exp"]
    assert response["iat"] == access_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(access_token.data["scope"])
           )
  end

  test "existing access token, correct token_type_hint param", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "access_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == access_token.data["iss"]
    assert response["client_id"] == access_token.data["client_id"]
    assert response["token_type"] == access_token.data["token_type"]
    assert response["exp"] == access_token.data["exp"]
    assert response["iat"] == access_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(access_token.data["scope"])
           )
  end

  test "existing access token, incorrect token_type_hint param", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == access_token.data["iss"]
    assert response["client_id"] == access_token.data["client_id"]
    assert response["token_type"] == access_token.data["token_type"]
    assert response["exp"] == access_token.data["exp"]
    assert response["iat"] == access_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(access_token.data["scope"])
           )
  end

  test "existing access token, but expired", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() - 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response == %{"active" => false}
  end

  test "existing access token, but not valid yet", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("nbf", now() + 1000)
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response == %{"active" => false}
  end

  test "revoked access token", %{conn: conn} do
    {:ok, access_token} =
      AccessToken.gen_new()
      |> AccessToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> AccessToken.put_value("client_id", "client_confidential_1")
      |> AccessToken.put_value("token_type", "strange_type")
      |> AccessToken.put_value("exp", now() + 3600)
      |> AccessToken.put_value("iat", now())
      |> AccessToken.put_value("status", "revoked")
      |> AccessToken.put_value("iss", "https://example.net")
      |> AccessToken.store(%{})

    req_body = %{
      "token" => access_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response == %{"active" => false}
  end

  ##########################################################################
  # Endpoint test - refresh tokens
  ##########################################################################

  test "existing refresh token, no token_type_hint param", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("token_type", "strange_type")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "password1"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == refresh_token.data["iss"]
    assert response["client_id"] == refresh_token.data["client_id"]
    assert response["token_type"] == refresh_token.data["token_type"]
    assert response["exp"] == refresh_token.data["exp"]
    assert response["iat"] == refresh_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(refresh_token.data["scope"])
           )

    assert response["__asteroid_oauth2_initial_flow"] == nil
  end

  test "existing refresh token, correct token_type_hint param", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("token_type", "strange_type")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == refresh_token.data["iss"]
    assert response["client_id"] == refresh_token.data["client_id"]
    assert response["token_type"] == refresh_token.data["token_type"]
    assert response["exp"] == refresh_token.data["exp"]
    assert response["iat"] == refresh_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(refresh_token.data["scope"])
           )

    assert response["__asteroid_oauth2_initial_flow"] == nil
  end

  test "existing refresh token, incorrect token_type_hint param", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("token_type", "strange_type")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "access_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response["active"] == true
    assert response["iss"] == refresh_token.data["iss"]
    assert response["client_id"] == refresh_token.data["client_id"]
    assert response["token_type"] == refresh_token.data["token_type"]
    assert response["exp"] == refresh_token.data["exp"]
    assert response["iat"] == refresh_token.data["iat"]

    assert Scope.Set.equal?(
             Scope.Set.from_scope_param!(response["scope"]),
             Scope.Set.new(refresh_token.data["scope"])
           )

    assert response["__asteroid_oauth2_initial_flow"] == nil
  end

  test "existing refresh token, but expired", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("token_type", "strange_type")
      |> RefreshToken.put_value("exp", now() - 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response == %{"active" => false}
  end

  test "existing refresh token, but not valid yet", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("token_type", "strange_type")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("nbf", now() + 1000)
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response == %{"active" => false}
  end

  test "revoked refresh token", %{conn: conn} do
    {:ok, refresh_token} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("scope", ["scp3", "scp9", "scp1"])
      |> RefreshToken.put_value("client_id", "client_confidential_1")
      |> RefreshToken.put_value("token_type", "strange_type")
      |> RefreshToken.put_value("exp", now() + 3600)
      |> RefreshToken.put_value("iat", now())
      |> RefreshToken.put_value("status", "revoked")
      |> RefreshToken.put_value("iss", "https://example.net")
      |> RefreshToken.put_value("__asteroid_oauth2_initial_flow", "ropc")
      |> RefreshToken.store(%{})

    req_body = %{
      "token" => refresh_token.id,
      "token_type_hint" => "refresh_token"
    }

    response =
      conn
      |> put_req_header("authorization", basic_auth_header("client_confidential_1", "<PASSWORD>"))
      |> post(Routes.introspect_path(conn, :handle), req_body)
      |> json_response(200)

    assert response == %{"active" => false}
  end

  ##########################################################################
  # Helper functions
  ##########################################################################

  defp basic_auth_header(client, secret) do
    "Basic " <> Base.encode64(client <> ":" <> secret)
  end
end


<|start_filename|>lib/asteroid/oauth2/endpoint.ex<|end_filename|>
defmodule Asteroid.OAuth2.Endpoint do
  @moduledoc """
  Util functions to work with OAuth2 endpoints
  """

  import Asteroid.Utils

  @type auth_method :: :none | :client_secret_basic | :tls_client_auth

  @typedoc """
  String representation of an endpoint client authentication method

  Must be the strict conversion of a `t:auth_method/0` atom
  """

  @type auth_method_str :: String.t()

  @doc """
  Returns the authentication methods supported by the token endpoint

  It scans the following configuration options and tries to find known `APIac.Authenticator`
  modules to determine autyhentication method support:
  - `:api_oauth2_plugs`
  - `:api_oauth2_endpoint_token_plugs`

  It also adds `:none` to the list since public client are authorized to access the token
  endpoint without authentication.
  """

  @spec token_endpoint_auth_methods_supported() :: [auth_method()]

  def token_endpoint_auth_methods_supported() do
    Enum.reduce(
      astrenv(:api_oauth2_plugs, []) ++ astrenv(:api_oauth2_endpoint_token_plugs, []),
      MapSet.new([:none]),
      fn
        {module, _options}, acc ->
          case apiac_authenticator_to_auth_method(module) do
            nil ->
              acc

            apiac_authenticator ->
              MapSet.put(acc, apiac_authenticator)
          end
      end
    )
    |> MapSet.to_list()
  end

  @doc """
  Returns the authentication methods supported by the revocation endpoint

  It scans the following configuration options and tries to find known `APIac.Authenticator`
  modules to determine autyhentication method support:
  - `:api_oauth2_plugs`
  - `:api_oauth2_endpoint_revoke_plugs`
  """

  @spec revoke_endpoint_auth_methods_supported() :: [auth_method()]

  def revoke_endpoint_auth_methods_supported() do
    Enum.reduce(
      astrenv(:api_oauth2_plugs, []) ++ astrenv(:api_oauth2_endpoint_revoke_plugs, []),
      MapSet.new(),
      fn
        {module, _options}, acc ->
          case apiac_authenticator_to_auth_method(module) do
            nil ->
              acc

            apiac_authenticator ->
              MapSet.put(acc, apiac_authenticator)
          end
      end
    )
    |> MapSet.to_list()
  end

  @doc """
  Returns the authentication methods supported by the introspection endpoint

  It scans the following configuration options and tries to find known `APIac.Authenticator`
  modules to determine autyhentication method support:
  - `:api_oauth2_plugs`
  - `:api_oauth2_endpoint_introspect_plugs`
  """

  @spec introspect_endpoint_auth_methods_supported() :: [auth_method()]

  def introspect_endpoint_auth_methods_supported() do
    Enum.reduce(
      astrenv(:api_oauth2_plugs, []) ++ astrenv(:api_oauth2_endpoint_introspect_plugs, []),
      MapSet.new(),
      fn
        {module, _options}, acc ->
          case apiac_authenticator_to_auth_method(module) do
            nil ->
              acc

            apiac_authenticator ->
              MapSet.put(acc, apiac_authenticator)
          end
      end
    )
    |> MapSet.to_list()
  end

  @doc """
  Returns the `t:auth_method/0` of an `APIac.Authenticator` or `nil` if it is not recognized

  The mapping is as follows:
  - `APIacAuthBasic`: `:client_secret_basic`
  - `APIacAuthMTLS`: `:tls_client_auth`
  """

  @spec apiac_authenticator_to_auth_method(module()) :: auth_method | nil

  def apiac_authenticator_to_auth_method(APIacAuthBasic), do: :client_secret_basic
  def apiac_authenticator_to_auth_method(APIacAuthClientSecretPost), do: :client_secret_post
  def apiac_authenticator_to_auth_method(APIacAuthMTLS), do: :tls_client_auth
  def apiac_authenticator_to_auth_method(_), do: nil
end


<|start_filename|>test/asteroid/oidc/authenticated_session_test.exs<|end_filename|>
defmodule Asteroid.OIDC.AuthenticatedSessionTest do
  use ExUnit.Case, async: true

  alias Asteroid.OIDC.AuthenticatedSession
  alias Asteroid.OIDC.AuthenticationEvent
  alias Asteroid.Token.RefreshToken

  test "OIDC associated refresh tokens are destroyed when no offline_access" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    {:ok, rt1} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("authenticated_session_id", as.id)
      |> RefreshToken.put_value("scope", ["openid"])
      |> RefreshToken.store()

    {:ok, rt2} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("authenticated_session_id", as.id)
      |> RefreshToken.put_value("scope", ["openid"])
      |> RefreshToken.store()

    AuthenticatedSession.delete(as)

    assert {:error, _} = RefreshToken.get(rt1.id)
    assert {:error, _} = RefreshToken.get(rt2.id)
  end

  test "Associated refresh tokens are not destroyed when not OIDC" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    {:ok, rt} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("authenticated_session_id", as.id)
      |> RefreshToken.store()

    AuthenticatedSession.delete(as)

    assert {:ok, _} = RefreshToken.get(rt.id)
  end

  test "OIDC associated refresh token is not destroyed when offline_access" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    {:ok, rt} =
      RefreshToken.gen_new()
      |> RefreshToken.put_value("authenticated_session_id", as.id)
      |> RefreshToken.put_value("scope", ["openid", "offline_access"])
      |> RefreshToken.store()

    AuthenticatedSession.delete(as)

    assert {:ok, _} = RefreshToken.get(rt.id)
  end

  test "acr set with 1 auth event associated" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "password")
    |> AuthenticationEvent.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert as.data["current_acr"] == "loa1"
  end

  test "acr set with 2 auth events associated" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "password")
    |> AuthenticationEvent.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "webauthn")
    |> AuthenticationEvent.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert as.data["current_acr"] == "loa2"
  end

  test "acr set with 1 auth event associated then auth event deleted, session destroyed" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    {:ok, ae} =
      AuthenticationEvent.gen_new(as.id)
      |> AuthenticationEvent.put_value("name", "password")
      |> AuthenticationEvent.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert as.data["current_acr"] == "loa1"

    AuthenticationEvent.delete(ae)

    assert {:error, as} = AuthenticatedSession.get(as.id)
  end

  test "acr set with 2 auth events associated then one deleted" do
    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "password")
    |> AuthenticationEvent.store()

    {:ok, ae} =
      AuthenticationEvent.gen_new(as.id)
      |> AuthenticationEvent.put_value("name", "webauthn")
      |> AuthenticationEvent.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert as.data["current_acr"] == "loa2"

    AuthenticationEvent.delete(ae)

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert as.data["current_acr"] == "loa1"
  end

  test "info function" do
    Process.put(:oidc_acr_config,
      "3-factor": [
        callback: &AsteroidWeb.LOA3_webflow.start_webflow/2,
        auth_event_set: [["password", "otp", "webauthn"]]
      ],
      "2-factor": [
        callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
        auth_event_set: [["password", "otp"], ["password", "<PASSWORD>"], ["web<PASSWORD>", "otp"]]
      ],
      "1-factor": [
        callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
        auth_event_set: [["password"], ["<PASSWORD>"]],
        default: true
      ]
    )

    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "password")
    |> AuthenticationEvent.put_value("amr", "pwd")
    |> AuthenticationEvent.put_value("time", 100_000)
    |> AuthenticationEvent.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "otp")
    |> AuthenticationEvent.put_value("amr", "otp")
    |> AuthenticationEvent.put_value("time", 200_000)
    |> AuthenticationEvent.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "otp")
    |> AuthenticationEvent.put_value("amr", "otp")
    |> AuthenticationEvent.put_value("time", 250_000)
    |> AuthenticationEvent.store()

    AuthenticationEvent.gen_new(as.id)
    |> AuthenticationEvent.put_value("name", "webauthn")
    |> AuthenticationEvent.put_value("amr", "phr")
    |> AuthenticationEvent.put_value("time", 300_000)
    |> AuthenticationEvent.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert as.data["current_acr"] == "3-factor"

    assert %{acr: "3-factor", amr: amr, auth_time: 300_000} = AuthenticatedSession.info(as.id)
    assert Enum.sort(amr) == ["otp", "phr", "pwd"]

    assert %{acr: "1-factor", amr: ["pwd"], auth_time: 100_000} ==
             AuthenticatedSession.info(as.id, "1-factor")

    assert %{acr: "2-factor", amr: amr, auth_time: 250_000} =
             AuthenticatedSession.info(as.id, "2-factor")

    assert Enum.sort(amr) == ["otp", "pwd"]

    assert %{acr: "3-factor", amr: amr, auth_time: 300_000} =
             AuthenticatedSession.info(as.id, "3-factor")

    assert Enum.sort(amr) == ["otp", "phr", "pwd"]
  end

  test "info function - no auth event associated" do
    Process.put(:oidc_acr_config,
      "3-factor": [
        callback: &AsteroidWeb.LOA3_webflow.start_webflow/2,
        auth_event_set: [["password", "otp", "webauthn"]]
      ],
      "2-factor": [
        callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
        auth_event_set: [["password", "otp"], ["password", "<PASSWORD>authn"], ["webauthn", "otp"]]
      ],
      "1-factor": [
        callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
        auth_event_set: [["password"], ["webauthn"]],
        default: true
      ]
    )

    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.put_value("current_acr", "2-factor")
      |> AuthenticatedSession.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert %{acr: "2-factor", amr: nil, auth_time: nil} == AuthenticatedSession.info(as.id)
  end

  test "info function - acr doesn't exist in config" do
    Process.put(:oidc_acr_config,
      "3-factor": [
        callback: &AsteroidWeb.LOA3_webflow.start_webflow/2,
        auth_event_set: [["password", "otp", "web<PASSWORD>"]]
      ],
      "2-factor": [
        callback: &AsteroidWeb.LOA2_webflow.start_webflow/2,
        auth_event_set: [["password", "<PASSWORD>"], ["password", "<PASSWORD>"], ["<PASSWORD>", "otp"]]
      ],
      "1-factor": [
        callback: &AsteroidWeb.LOA1_webflow.start_webflow/2,
        auth_event_set: [["password"], ["<PASSWORD>"]],
        default: true
      ]
    )

    {:ok, as} =
      AuthenticatedSession.gen_new("user1id")
      |> AuthenticatedSession.put_value("current_acr", "some_inexistant_acr")
      |> AuthenticatedSession.store()

    {:ok, as} = AuthenticatedSession.get(as.id)

    assert %{acr: "some_inexistant_acr", amr: nil, auth_time: nil} ==
             AuthenticatedSession.info(as.id)
  end
end


<|start_filename|>lib/asteroid/oauth2/device_authorization/rate_limiter.ex<|end_filename|>
defmodule Asteroid.OAuth2.DeviceAuthorization.RateLimiter do
  @moduledoc """
  Behaviour for rate limiters in the device authorization flow
  """

  alias Asteroid.OAuth2.DeviceAuthorization

  @type opts :: Keyword.t()

  @doc """
  Returns `:ok` if the given device code is not rate limited, `{:error, retry_after}` otherwise

  The retry after variable is a non negative number expressed in seconds, or `nil` if the
  backend has no capability to give that number.
  """

  @callback check(DeviceAuthorization.device_code(), opts()) ::
              :ok
              | {:rate_limited, non_neg_integer()}
              | {:rate_limited, nil}
end


<|start_filename|>lib/asteroid/config.ex<|end_filename|>
defmodule Asteroid.Config do
  @moduledoc """
  Specification of configuration options and callbacks
  """

  require Asteroid.Config.Builder

  alias Asteroid.Client
  alias Asteroid.Crypto
  alias Asteroid.OIDC
  alias Asteroid.Subject

  @typedoc """
  A map describing scope configuration

  The map keys are the scope's names. The map values are `Keyword.t/0` with the following
  options:
  - `:auto`: if true, the scope is automatically granted

  ## Example
  ```elixir
  %{
    "scope-a" => [auto: true],
    "scope-b" => [auto: true],
    "scope-c" => [auto: false],
    "scope-d" => [],
    "scope-f" => [auto: true],
  }
  ```
  """

  @type scope_config :: map()

  Asteroid.Config.Builder.defconfig do
    @doc """
    Access token store configuration

    #### Options
    - `:module`: the name of the module implementing the token's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all token's implementation functions. Refer to
    the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_access_token, [
      module: Asteroid.ObjectStore.AccessToken.Riak,
      opts: [bucket_type: "ephemeral_token", purge_interval: 10]
    ]
    ```
    """

    @type object_store_access_token :: Keyword.t()

    field(:object_store_access_token,
      config_time: :runtime
    )

    @doc """
    Refresh token store configuration

    #### Options
    - `:module`: the name of the module implementing the token's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all token's implementation functions. Refer to
    the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_refresh_token, [
      module: Asteroid.ObjectStore.RefreshToken.Mnesia
    ]
    ```
    """

    @type object_store_refresh_token :: Keyword.t()

    field(:object_store_refresh_token,
      config_time: :runtime
    )

    @doc """
    Authorization code store configuration

    #### Options
    - `:module`: the name of the module implementing the token's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all token's implementation functions. Refer to
    the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_authorization_code, [
      module: Asteroid.ObjectStore.AuthorizationCode.Mnesia
    ]
    ```
    """

    @type object_store_authorization_code :: Keyword.t()

    field(:object_store_authorization_code,
      config_time: :runtime
    )

    @doc """
    Device code store configuration

    #### Options
    - `:module`: the name of the module implementing the token's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all token's implementation functions. Refer to
    the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_device_code, [
      module: Asteroid.ObjectStore.DeviceCode.Mnesia
    ]
    ```
    """

    @type object_store_device_code :: Keyword.t()

    field(:object_store_device_code,
      config_time: :runtime
    )

    @doc """
    Request object store configuration

    #### Options
    - `:module`: the name of the module implementing the token's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all token's implementation functions. Refer to
    the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_request_object, [
      module: Asteroid.ObjectStore.GenericKV.Mnesia
    ]
    ```
    """

    @type object_store_request_object :: Keyword.t()

    field(:object_store_request_object,
      config_time: :runtime
    )

    @doc """
    Authenticated session store configuration

    #### Options
    - `:module`: the name of the module implementing the object's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all object's implementation functions. Refer
    to the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_authenticated_session, [
      module: Asteroid.ObjectStore.AuthenticatedSession.Mnesia
    ]
    ```
    """

    @type object_store_authenticated_session :: Keyword.t()

    field(:object_store_authenticated_session,
      config_time: :runtime
    )

    @doc """
    Authentication event store configuration

    #### Options
    - `:module`: the name of the module implementing the object's behaviours. No default,
    **mandatory**
    - `:opts`: options that will be passed to the all object's implementation functions. Refer
    to the implementation documentation. Defaults to `[]`
    - `:auto_install`: `boolean()` indicating whether the `install/1` callback of the
    impementation should be called at Asteroid startup. Defaults to `true`
    - `:auto_start`: `boolean()` indicating whether the `start_link/1` or `start/1` callback of
    the Implementation should be called at Asteroid startup. Defaults to `true`

    #### Example

    ```elixir
    config :asteroid, :object_store_authenticated_session, [
      module: Asteroid.ObjectStore.AuthenticationEvent.Mnesia
    ]
    ```
    """

    @type object_store_authentication_event :: Keyword.t()

    field(:object_store_authentication_event,
      config_time: :runtime
    )

    @doc """
    Callback invoked before storing a refresh token
    """

    @type object_store_refresh_token_before_store_callback ::
            (Asteroid.Token.RefreshToken.t(), Asteroid.Context.t() ->
               Asteroid.Token.RefreshToken.t())

    field(:object_store_refresh_token_before_store_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked before storing an access token
    """

    @type object_store_access_token_before_store_callback ::
            (Asteroid.Token.AccessToken.t(), Asteroid.Context.t() ->
               Asteroid.Token.RefreshToken.t())

    field(:object_store_access_token_before_store_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked before storing an authorization code
    """

    @type object_store_authorization_code_before_store_callback ::
            (Asteroid.Token.AuthorizationCode.t(), Asteroid.Context.t() ->
               Asteroid.Token.AuthorizationCode.t())

    field(:object_store_authorization_code_before_store_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked before storing a device code
    """

    @type object_store_device_code_before_store_callback ::
            (Asteroid.Token.DeviceCode.t(), Asteroid.Context.t() ->
               Asteroid.Token.DeviceCode.t())

    field(:object_store_device_code_before_store_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked before storing authenticated session
    """

    @type object_store_authenticated_session_before_store_callback ::
            (Asteroid.OIDC.AuthenticatedSession.t(), Asteroid.Context.t() ->
               Asteroid.OIDC.AuthenticatedSession.t())

    field(:object_store_authenticated_session_before_store_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked before storing authentication event
    """

    @type object_store_authentication_event_before_store_callback ::
            (Asteroid.OIDC.AuthenticationEvent.t(), Asteroid.Context.t() ->
               Asteroid.OIDC.AuthenticationEvent.t())

    field(:object_store_authentication_event_before_store_callback,
      config_time: :runtime
    )

    @doc """
    Plugs installed on `"/api/oauth2"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oauth2_plugs :: [{module(), Keyword.t()}]

    field(:api_oauth2_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `"/api/oauth2/token"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oauth2_endpoint_token_plugs :: [{module(), Keyword.t()}]

    field(:api_oauth2_endpoint_token_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `"/api/oauth2/introspect"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oauth2_endpoint_introspect_plugs :: [{module(), Keyword.t()}]

    field(:api_oauth2_endpoint_introspect_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `"/api/oauth2/revoke"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oauth2_endpoint_revoke_plugs :: [{module(), Keyword.t()}]

    field(:api_oauth2_endpoint_revoke_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `"/api/oauth2/register"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oauth2_endpoint_register_plugs :: [{module(), Keyword.t()}]

    field(:api_oauth2_endpoint_register_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `"/api/oauth2/device_authorization"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oauth2_endpoint_device_authorization_plugs :: [{module(), Keyword.t()}]

    field(:api_oauth2_endpoint_device_authorization_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `/.well-known/`

    See also [protecting APIs](protecting-apis.html)
    """

    @type well_known_plugs :: [{module(), Keyword.t()}]

    field(:well_known_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `/discovery/`

    See also [protecting APIs](protecting-apis.html)
    """

    @type discovery_plugs :: [{module(), Keyword.t()}]

    field(:discovery_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on browser pathes

    See also [protecting APIs](protecting-apis.html)
    """

    @type browser_plugs :: [{module(), Keyword.t()}]

    field(:browser_plugs,
      config_time: :compile
    )

    @doc """
    List of enabled grant types

    It is used in OAuth2 APIs (such as `/token`) so as to determine support, and for metadata
    generation.
    """

    @type oauth2_grant_types_enabled :: [Asteroid.OAuth2.grant_type()]

    field(:oauth2_grant_types_enabled,
      config_time: :runtime
    )

    @doc """
    List of enabled response types

    It is used in OAuth2 web authentication flows (`/authorize`) so as to determine support,
    and for metadata generation.
    """

    @type oauth2_response_types_enabled :: [Asteroid.OAuth2.response_type()]

    field(:oauth2_response_types_enabled,
      config_time: :runtime
    )

    @doc """
    Callback to verify username and password in the ROPC flow.
    """

    @typedoc """
    Callback function for the `:oauth2_flow_ropc_username_password_verify_callback` configuration
    option.

    Calls the callback `callback` with `callback.(conn, username, password)`
    """

    @type oauth2_ropc_username_password_verify_callback ::
            (Plug.Conn.t(), String.t(), String.t() ->
               {:ok, Asteroid.Subject.t()} | {:error, Exception.t()})

    field(:oauth2_flow_ropc_username_password_verify_callback,
      config_time: :runtime
    )

    @doc """
    Verbosity level for the API's error messages

    The `:debug` level can return information useful to attackers. The `:minimal` level can
    break the specification's support.
    """

    @type api_error_response_verbosity :: :debug | :normal | :minimal

    field(:api_error_response_verbosity,
      config_time: :runtime
    )

    @doc """
    Scope configuration for the ROPC flow
    """

    @type oauth2_flow_ropc_scope_config :: scope_config()

    field(:oauth2_flow_ropc_scope_config,
      config_time: :runtime,
      used_by: [:oauth2_scope_callback]
    )

    @doc """
    Callback called to set scopes according to the configuration
    """

    @type oauth2_scope_callback ::
            (OAuth2Utils.Scope.Set.t(), Asteroid.Context.t() -> OAuth2Utils.Scope.Set.t())

    field(:oauth2_scope_callback,
      config_time: :runtime,
      uses: [
        :oauth2_flow_ropc_scope_config,
        :oauth2_flow_client_credentials_scope_config,
        :oauth2_flow_device_authorization_scope_config,
        :oidc_flow_authorization_code_scope_config,
        :oidc_flow_implicit_scope_config,
        :oidc_flow_hybrid_scope_config
      ]
    )

    @doc """
    Defines whether a refresh token should be issued when initiating an ROPC flow
    """

    @type oauth2_flow_ropc_issue_refresh_token_init :: boolean()

    field(:oauth2_flow_ropc_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens in the ROPC flow
    """

    @type oauth2_flow_ropc_issue_refresh_token_refresh :: boolean()

    field(:oauth2_flow_ropc_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Callback called to determine whether a refresh token should be issued

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_issue_refresh_token_callback :: (Asteroid.Context.t() -> boolean())

    field(:oauth2_issue_refresh_token_callback,
      config_time: :runtime,
      uses: [
        :oauth2_issue_refresh_token_init,
        :oauth2_issue_refresh_token_refresh,
        :oauth2_flow_ropc_issue_refresh_token_init,
        :oauth2_flow_ropc_issue_refresh_token_refresh,
        :oauth2_flow_client_credentials_issue_refresh_token_init,
        :oauth2_flow_client_credentials_issue_refresh_token_refresh,
        :oauth2_flow_authorization_code_issue_refresh_token_init,
        :oauth2_flow_authorization_code_issue_refresh_token_refresh,
        :oauth2_flow_device_authorization_issue_refresh_token_init,
        :oauth2_flow_device_authorization_issue_refresh_token_refresh,
        :oidc_flow_authorization_code_issue_refresh_token_init,
        :oidc_flow_authorization_code_issue_refresh_token_refresh,
        :oidc_flow_hybrid_issue_refresh_token_init,
        :oidc_flow_hybrid_issue_refresh_token_refresh
      ]
    )

    @doc """
    Defines the lifetime of a refresh token in the ROPC flow
    """

    @type oauth2_flow_ropc_refresh_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_ropc_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Callback called to determine the lifetime of a refresh token

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_refresh_token_lifetime_callback :: (Asteroid.Context.t() -> non_neg_integer())

    field(:oauth2_refresh_token_lifetime_callback,
      config_time: :runtime,
      uses: [
        :oauth2_refresh_token_lifetime,
        :oauth2_flow_ropc_refresh_token_lifetime,
        :oauth2_flow_client_credentials_refresh_token_lifetime,
        :oauth2_flow_authorization_code_refresh_token_lifetime,
        :oauth2_flow_device_authorization_refresh_token_lifetime,
        :oidc_flow_authorization_code_refresh_token_lifetime,
        :oidc_flow_hybrid_refresh_token_lifetime
      ]
    )

    @doc """
    Defines the lifetime of an access token in the ROPC flow
    """

    @type oauth2_flow_ropc_access_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_ropc_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token in the ROPC flow
    """

    @type oauth2_flow_ropc_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oauth2_flow_ropc_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing key name of an access token in the ROPC flow
    """

    @type oauth2_flow_ropc_access_token_signing_key :: Crypto.Key.name()

    field(:oauth2_flow_ropc_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the ROPC flow
    """

    @type oauth2_flow_ropc_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_flow_ropc_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Callback called to determine the lifetime of an access token

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_access_token_lifetime_callback :: (Asteroid.Context.t() -> non_neg_integer())

    field(:oauth2_access_token_lifetime_callback,
      config_time: :runtime,
      uses: [
        :oauth2_access_token_lifetime,
        :oauth2_flow_ropc_access_token_lifetime,
        :oauth2_flow_client_credentials_access_token_lifetime,
        :oauth2_flow_authorization_code_access_token_lifetime,
        :oauth2_flow_implicit_access_token_lifetime,
        :oauth2_flow_device_authorization_access_token_lifetime,
        :oidc_flow_authorization_code_access_token_lifetime,
        :oidc_flow_implicit_access_token_lifetime,
        :oidc_flow_hybrid_access_token_lifetime
      ]
    )

    @doc """
    Callback called to determine the serialization format of an access token

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_access_token_serialization_format_callback ::
            (Asteroid.Context.t() -> Asteroid.Token.serialization_format())

    field(:oauth2_access_token_serialization_format_callback,
      config_time: :runtime,
      uses: [
        :oauth2_access_token_serialization_format,
        :oauth2_flow_ropc_access_token_serialization_format,
        :oauth2_flow_client_credentials_access_token_serialization_format,
        :oauth2_flow_authorization_code_access_token_serialization_format,
        :oauth2_flow_implicit_access_token_serialization_format,
        :oauth2_flow_device_authorization_access_token_serialization_format,
        :oidc_flow_authorization_code_access_token_serialization_format,
        :oidc_flow_implicit_access_token_serialization_format,
        :oidc_flow_hybrid_access_token_serialization_format
      ]
    )

    @doc """
    Callback called to determine the signing key name of an access token

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_access_token_signing_key_callback ::
            (Asteroid.Context.t() -> Crypto.Key.name())

    field(:oauth2_access_token_signing_key_callback,
      config_time: :runtime,
      uses: [
        :oauth2_access_token_signing_key,
        :oauth2_flow_ropc_access_token_signing_key,
        :oauth2_flow_client_credentials_access_token_signing_key,
        :oauth2_flow_authorization_code_access_token_signing_key,
        :oauth2_flow_implicit_access_token_signing_key,
        :oauth2_flow_device_authorization_access_token_signing_key,
        :oidc_flow_authorization_code_access_token_signing_key,
        :oidc_flow_implicit_access_token_signing_key,
        :oidc_flow_hybrid_access_token_signing_key
      ]
    )

    @doc """
    Callback called to determine the signing algorithm of an access token

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_access_token_signing_alg_callback ::
            (Asteroid.Context.t() -> Crypto.Key.jws_alg())

    field(:oauth2_access_token_signing_alg_callback,
      config_time: :runtime,
      uses: [
        :oauth2_access_token_signing_alg,
        :oauth2_flow_ropc_access_token_signing_alg,
        :oauth2_flow_client_credentials_access_token_signing_alg,
        :oauth2_flow_authorization_code_access_token_signing_alg,
        :oauth2_flow_implicit_access_token_signing_alg,
        :oauth2_flow_device_authorization_access_token_signing_alg,
        :oidc_flow_authorization_code_access_token_signing_alg,
        :oidc_flow_implicit_access_token_signing_alg,
        :oidc_flow_hybrid_access_token_signing_alg
      ]
    )

    @doc """
    Callback called to determine the lifetime of an authorization code

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oauth2_authorization_code_lifetime_callback ::
            (Asteroid.Context.t() -> non_neg_integer())

    field(:oauth2_authorization_code_lifetime_callback,
      config_time: :runtime,
      uses: [
        :oauth2_authorization_code_lifetime,
        :oauth2_flow_authorization_code_authorization_code_lifetime,
        :oidc_flow_authorization_code_authorization_code_lifetime,
        :oidc_flow_hybrid_authorization_code_lifetime
      ]
    )

    @doc """
    Defines the lifetime of an authorization code in the code flow
    """

    @type oauth2_flow_authorization_code_authorization_code_lifetime :: non_neg_integer()

    field(:oauth2_flow_authorization_code_authorization_code_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_authorization_code_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Callback invoked on the json response when the grant_type is "password"
    """

    @type oauth2_endpoint_token_grant_type_password_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_token_grant_type_password_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response when the grant_type is "password"
    """

    @type oauth2_endpoint_token_grant_type_password_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_token_grant_type_password_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the json response when the grant_type is "refresh_token"
    """

    @type oauth2_endpoint_token_grant_type_refresh_token_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_token_grant_type_refresh_token_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response when the grant_type is "refresh_token"
    """

    @type oauth2_endpoint_token_grant_type_refresh_token_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_token_grant_type_refresh_token_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked to determine if a client is authorized to introspect tokens on the
    `"/introspect"` endpoint
    """

    @type oauth2_endpoint_introspect_client_authorized ::
            (Asteroid.Client.t() -> boolean())

    field(:oauth2_endpoint_introspect_client_authorized,
      config_time: :runtime
    )

    @doc """
    Defines the default claims to be returned from the `"/introspect"` endpoint

    Note that client's configuration takes precedence over this configuration option.
    """

    @type oauth2_endpoint_introspect_claims_resp :: [String.t()]

    field(:oauth2_endpoint_introspect_claims_resp,
      config_time: :runtime,
      used_by: [:oauth2_endpoint_introspect_claims_resp_callback]
    )

    @doc """
    Callback invoked to determine the claims to be returned from the `"/introspect"` endpoint
    """

    @type oauth2_endpoint_introspect_claims_resp_callback ::
            (Asteroid.Client.t() -> [String.t()])

    field(:oauth2_endpoint_introspect_claims_resp_callback,
      config_time: :runtime,
      uses: [:oauth2_endpoint_introspect_claims_resp]
    )

    @doc """
    Callback invoked on the json response on the `"/introspect"` endpoint
    """

    @type oauth2_endpoint_introspect_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_introspect_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response on the `"/introspect"` endpoint
    """

    @type oauth2_endpoint_introspect_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_introspect_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Scope configuration for the client credentials flow
    """

    @type oauth2_flow_client_credentials_scope_config :: scope_config()

    field(:oauth2_flow_client_credentials_scope_config,
      config_time: :runtime,
      used_by: [:oauth2_scope_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when initiating a client credentials
    flow

    Note that you should note, according to the specification, release a refresh token in
    this flow.
    """

    @type oauth2_flow_client_credentials_issue_refresh_token_init :: boolean()

    field(:oauth2_flow_client_credentials_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens
    """

    @type oauth2_flow_client_credentials_issue_refresh_token_refresh :: boolean()

    field(:oauth2_flow_client_credentials_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines the lifetime of a refresh token in the clienjt credentials flow
    """

    @type oauth2_flow_client_credentials_refresh_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_client_credentials_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an access token in the client credentials flow
    """

    @type oauth2_flow_client_credentials_access_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_client_credentials_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token in the client credentials flow
    """

    @type oauth2_flow_client_credentials_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oauth2_flow_client_credentials_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing key name of an access token in the client credentials flow
    """

    @type oauth2_flow_client_credentials_access_token_signing_key :: Crypto.Key.name()

    field(:oauth2_flow_client_credentials_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the client credentials flow
    """

    @type oauth2_flow_client_credentials_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_flow_client_credentials_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Callback invoked on the json response when the grant_type is `"client_credentials"`
    """

    @type oauth2_endpoint_token_grant_type_client_credentials_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_token_grant_type_client_credentials_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response when the grant_type is
    `"client_credentials"`
    """

    @type oauth2_endpoint_token_grant_type_client_credentials_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_token_grant_type_client_credentials_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Default callback invoked on the `/authorize` endpoint to trigger the web authorization
    process flow for the OAuth2 authorization code flow

    This workflow is in charge of authenticating and authorizing (scopes...) the user in regards
    to the request. It will typically involve several step, i.e. display of web pages. It does
    returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the process,
    one of these callback shall be called:
    - `AsteroidWeb.AuthorizeController.authorization_granted/2`
    - `AsteroidWeb.AuthorizeController.authorization_denied/2`
    """

    @type oauth2_flow_authorization_code_web_authorization_callback ::
            AsteroidWeb.AuthorizeController.web_authorization_callback()

    field(:oauth2_flow_authorization_code_web_authorization_callback,
      config_time: :runtime,
      used_by: [:web_authorization_callback]
    )

    @doc """
    Callback invoked on the `t:Asteroid.OAuth2.RedirectUri.t/0` response on the `/authorize`
    endpoint
    """

    @type oauth2_endpoint_authorize_before_send_redirect_uri_callback ::
            (Asteroid.OAuth2.RedirectUri.t(), Asteroid.Context.t() ->
               Asteroid.OAuth2.RedirectUri.t())

    field(:oauth2_endpoint_authorize_before_send_redirect_uri_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response on the `/authorize` endpoint

    The connection is redirected immediatly after this callback returns.
    """

    @type oauth2_endpoint_authorize_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_authorize_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Defines whether a refresh token should be issued when submitting an authorization code
    in the authorization code flow
    """

    @type oauth2_flow_authorization_code_issue_refresh_token_init :: boolean()

    field(:oauth2_flow_authorization_code_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens in the authorization
    code flow
    """

    @type oauth2_flow_authorization_code_issue_refresh_token_refresh :: boolean()

    field(:oauth2_flow_authorization_code_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines the lifetime of a refresh token in the authorization code flow
    """

    @type oauth2_flow_authorization_code_refresh_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_authorization_code_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an access token in the authorization code flow
    """

    @type oauth2_flow_authorization_code_access_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_authorization_code_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token in the authorization code flow
    """

    @type oauth2_flow_authorization_code_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oauth2_flow_authorization_code_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing key name of an access token in the authorization code flow
    """

    @type oauth2_flow_authorization_code_access_token_signing_key :: Crypto.Key.name()

    field(:oauth2_flow_authorization_code_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the authorization code flow
    """

    @type oauth2_flow_authorization_code_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_flow_authorization_code_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Callback invoked on the json response when the grant_type is "authorization_code"
    """

    @type oauth2_endpoint_token_grant_type_authorization_code_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_token_grant_type_authorization_code_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response when the grant_type is
    "authorization_code"
    """

    @type oauth2_endpoint_token_grant_type_authorization_code_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_token_grant_type_authorization_code_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` in the `/revoke` endpoint
    """

    @type oauth2_endpoint_revoke_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_revoke_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Default callback invoked on the `/authorize` endpoint to trigger the web authorization
    process flow for the OAuth2 implicit flow

    This workflow is in charge of authenticating and authorizing (scopes...) the user in regards
    to the request. It will typically involve several step, i.e. display of web pages. It does
    returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the process,
    one of these callback shall be called:
    - `AsteroidWeb.AuthorizeController.authorization_granted/2`
    - `AsteroidWeb.AuthorizeController.authorization_denied/2`
    """

    @type oauth2_flow_implicit_web_authorization_callback ::
            (Plug.Conn.t(), AsteroidWeb.AuthorizeController.Request.t() -> Plug.Conn.t())

    field(:oauth2_flow_implicit_web_authorization_callback,
      config_time: :runtime,
      used_by: [
        :web_authorization_callback
      ]
    )

    @doc """
    Defines the lifetime of an access token in the implicit flow
    """

    @type oauth2_flow_implicit_access_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_implicit_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token in the implicit flow
    """

    @type oauth2_flow_implicit_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oauth2_flow_implicit_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing key name of an access token in the implicit flow
    """

    @type oauth2_flow_implicit_access_token_signing_key :: Crypto.Key.name()

    field(:oauth2_flow_implicit_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the implicit flow
    """

    @type oauth2_flow_implicit_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_flow_implicit_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    The PKCE policy

    This configuration option can have 3 values:
    - `:disabled`: PKCE support is disabled
    - `:mandatory`: all requests using the authorization code flow must use PKCE
    - `:optional`: use of PKCE is optional, except for clients marked as forced to use it
    """

    @type oauth2_pkce_policy :: :disabled | :optional | :mandatory

    field(:oauth2_pkce_policy,
      config_time: :runtime
    )

    @doc """
    Code challenge methods supported

    Supported methods are the following atoms:
    - `:plain`
    - `:S256`
    """

    @type oauth2_pkce_allowed_methods :: [atom()]

    field(:oauth2_pkce_allowed_methods,
      config_time: :runtime
    )

    @doc """
    Returns `true` if the client has to use PKCE, `false` otherwise

    Used only when the `:oauth2_pkce_policy` configuration option is set to `:optional`
    """

    @type oauth2_pkce_must_use_callback ::
            (Client.t() -> boolean())

    field(:oauth2_pkce_must_use_callback,
      config_time: :runtime
    )

    @doc """
    Callback called to determine whether a client is authorized to create new clients on
    the register endpoint or not
    """

    @type oauth2_endpoint_register_authorization_callback ::
            (Plug.Conn.t(), Asteroid.Client.t() ->
               :ok | {:error, Exception.t()})

    field(:oauth2_endpoint_register_authorization_callback,
      config_time: :runtime,
      uses: [:oauth2_endpoint_register_authorization_policy]
    )

    @doc """
    The client registration policy

    This configuration option can have 3 values:
    - `:all`: all clients are allowed to register new clients. Be careful when using this
    value because public clients and the clients created by these public clients could DDOS the
    client registration endpoint. You might consider severely rate-limiting these requests in
    this case
    - `:authenticated_clients`: only authenticated clients are allowed to create new clients
    - `:authorized_clients`: clients that have the `"asteroid.register"` scope set or that
    authenticate to that endpoint with an access token containing that scope
    """

    @type oauth2_endpoint_register_authorization_policy ::
            :all
            | :authenticated_clients
            | :authorized_clients

    field(:oauth2_endpoint_register_authorization_policy,
      config_time: :runtime,
      used_by: [:oauth2_endpoint_register_authorization_callback]
    )

    @doc """
    Additional fields that are saved when registering new clients

    Note that this option is overriden by client configuration, if existing.
    """

    @type oauth2_endpoint_register_additional_metadata_field :: [String.t()]

    field(:oauth2_endpoint_register_additional_metadata_field,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the json response when on the register endpoint
    """

    @type oauth2_endpoint_register_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_register_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response on the register endpoint
    """

    @type oauth2_endpoint_register_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_register_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Asteroid.Client.t()` before it's being saved
    """

    @type oauth2_endpoint_register_client_before_save_callback ::
            (Client.t(), Asteroid.Context.t() -> Client.t())

    field(:oauth2_endpoint_register_client_before_save_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked to generate the client id of a newly created client

    The callback should ensure that the client id does not already exists.
    """

    @type oauth2_endpoint_register_gen_client_id_callback ::
            (map(), Asteroid.Context.t() -> String.t())

    field(:oauth2_endpoint_register_gen_client_id_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked to generate the client *resource* id of a newly created client
    """

    @type oauth2_endpoint_register_gen_client_resource_id_callback ::
            (map(), Asteroid.Context.t() -> AttributeRepository.resource_id())

    field(:oauth2_endpoint_register_gen_client_resource_id_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked to determine the client type
    """

    @type oauth2_endpoint_register_client_type_callback ::
            (Client.t() -> Asteroid.OAuth2.Client.type())

    field(:oauth2_endpoint_register_client_type_callback,
      config_time: :runtime
    )

    @doc """
    Callback called to determine the supported authentication of the token endpoint
    """

    @type oauth2_endpoint_token_auth_methods_supported_callback ::
            (() -> [Asteroid.OAuth2.Endpoint.auth_method()])

    field(:oauth2_endpoint_token_auth_methods_supported_callback,
      config_time: :runtime
    )

    @doc """
    OAuth2 metadata service documentation URL
    """

    @type oauth2_endpoint_metadata_service_documentation :: String.t()

    field(:oauth2_endpoint_metadata_service_documentation,
      config_time: :runtime
    )

    @doc """
    OAuth2 metadata UI locales supported
    """

    @type oauth2_endpoint_metadata_ui_locales_supported :: [String.t()]

    field(:oauth2_endpoint_metadata_ui_locales_supported,
      config_time: :runtime
    )

    @doc """
    OAuth2 metadata OP policy URL
    """

    @type oauth2_endpoint_metadata_op_policy_uri :: String.t()

    field(:oauth2_endpoint_metadata_op_policy_uri,
      config_time: :runtime
    )

    @doc """
    OAuth2 metadata OP tos URL
    """

    @type oauth2_endpoint_metadata_op_tos_uri :: String.t()

    field(:oauth2_endpoint_metadata_op_tos_uri,
      config_time: :runtime
    )

    @doc """
    Metadata fields to be signed

    The configuration option can have 3 values:
    - `:disabled`: no metadata fields are signed
    - `:all`: all fields are signed
    - `[String.t()]`: a list of fields to be included in the signed statement
    """

    @type oauth2_endpoint_metadata_signed_fields :: :disabled | :all | [String.t()]

    field(:oauth2_endpoint_metadata_signed_fields,
      config_time: :runtime
    )

    @doc """
    Key name for the signed metadata fields
    """

    @type oauth2_endpoint_metadata_signing_key :: Crypto.Key.name()

    field(:oauth2_endpoint_metadata_signing_key,
      config_time: :runtime
    )

    @doc """
    Key algorithm for the signed metadata fields
    """

    @type oauth2_endpoint_metadata_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_endpoint_metadata_signing_alg,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the json response on the `/.well-known/oauth-authorization-server`
    endpoint

    Note that this callback is called before optional signature of metadata fields, so that
    added fields can be signed as well.
    """

    @type oauth2_endpoint_metadata_before_send_resp_callback :: (map() -> map())

    field(:oauth2_endpoint_metadata_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response on the
    `/.well-known/oauth-authorization-server` endpoint
    """

    @type oauth2_endpoint_metadata_before_send_conn_callback ::
            (Plug.Conn.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_metadata_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the json response on the `/discovery/keys` endpoint
    """

    @type oauth2_endpoint_discovery_keys_before_send_resp_callback :: (map() -> map())

    field(:oauth2_endpoint_discovery_keys_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response on the `/discovery/keys` endpoint
    """

    @type oauth2_endpoint_discovery_keys_before_send_conn_callback ::
            (Plug.Conn.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_discovery_keys_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Cryptographic keys configuration

    Refer to `t:Asteroid.Crypto.Key.key_config/0` for more information.

    **Security consideration**: consider storing keys in a separate configuration file
    (such as `secret.exs`).
    """

    @type crypto_keys :: Crypto.Key.key_config()

    field(:crypto_keys,
      config_time: :runtime
    )

    @doc """
    Cryptographic keys cache store

    The first element is a module implementing the `Asteroid.Crypto.Key.Cache` behaviour, and
    the second element are the module's options.
    """

    @type crypto_keys_cache :: {module(), Crypto.Key.Cache.opts()}

    field(:crypto_keys_cache,
      config_time: :runtime
    )

    @doc """
    Determines whether the `"none"` JWS algorithm is supported

    It is set using the `JOSE.JWA.unsecured_signing/1` function on Asteroid startup. Defaults
    to `false`.
    """

    @type crypto_jws_none_alg_enabled :: boolean()

    field(:crypto_jws_none_alg_enabled,
      config_time: :runtime
    )

    @doc """
    Scope configuration for the device authorization flow
    """

    @type oauth2_flow_device_authorization_scope_config :: scope_config()

    field(:oauth2_flow_device_authorization_scope_config,
      config_time: :runtime,
      used_by: [:oauth2_scope_callback]
    )

    @doc """
    Callback invoked on the json response when the grant_type is
    "urn:ietf:params:oauth:grant-type:device_code"
    """

    @type oauth2_endpoint_device_authorization_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_device_authorization_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response when the grant_type is
    "urn:ietf:params:oauth:grant-type:device_code"
    """

    @type oauth2_endpoint_device_authorization_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_device_authorization_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Defines the lifetime of a device code in the device authorization flow
    """

    @type oauth2_flow_device_authorization_device_code_lifetime :: non_neg_integer()

    field(:oauth2_flow_device_authorization_device_code_lifetime,
      config_time: :runtime,
      unit: "seconds"
    )

    @doc """
    callback to generate the user code
    """

    @type oauth2_flow_device_authorization_user_code_callback ::
            (Asteroid.Context.t() -> String.t())

    field(:oauth2_flow_device_authorization_user_code_callback,
      config_time: :runtime,
      unit: "seconds"
    )

    @doc """
    Callback invoked on the `/device` endpoint to trigger the web authorization process flow
    for the OAuth2 device authorization flow

    This workflow is in charge of validating the user code, as well as authenticating and
    authorizing (scopes...) the request. It will typically involve several step, i.e.
    user code confirmation, authentication and optionnaly accepting scope through web pages.
    It returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the
    process, one of these callback shall be called:
    - `AsteroidWeb.DeviceController.authorization_granted/2`
    - `AsteroidWeb.DeviceController.authorization_denied/2`
    """

    @type oauth2_flow_device_authorization_web_authorization_callback ::
            (Plug.Conn.t(), AsteroidWeb.DeviceController.Request.t() -> Plug.Conn.t())

    field(:oauth2_flow_device_authorization_web_authorization_callback,
      config_time: :runtime
    )

    @doc """
    Defines whether a refresh token should be issued when initiating a device authorization
    flow
    """

    @type oauth2_flow_device_authorization_issue_refresh_token_init :: boolean()

    field(:oauth2_flow_device_authorization_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens in the device
    authorization flow
    """

    @type oauth2_flow_device_authorization_issue_refresh_token_refresh :: boolean()

    field(:oauth2_flow_device_authorization_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines the lifetime of a refresh token in the device authorization flow
    """

    @type oauth2_flow_device_authorization_refresh_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_device_authorization_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token in the device authorization flow
    """

    @type oauth2_flow_device_authorization_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oauth2_flow_device_authorization_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing key name of an access token in the device authorization flow
    """

    @type oauth2_flow_device_authorization_access_token_signing_key :: Crypto.Key.name()

    field(:oauth2_flow_device_authorization_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the device authorization flow
    """

    @type oauth2_flow_device_authorization_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_flow_device_authorization_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Defines the lifetime of an access token in the device authorization flow
    """

    @type oauth2_flow_device_authorization_access_token_lifetime :: non_neg_integer()

    field(:oauth2_flow_device_authorization_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Callback invoked on the json response when the grant_type is
    "urn:ietf:params:oauth:grant-type:device_code"
    """

    @type oauth2_endpoint_token_grant_type_device_code_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oauth2_endpoint_token_grant_type_device_code_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response when the grant_type is
    "urn:ietf:params:oauth:grant-type:device_code"
    """

    @type oauth2_endpoint_token_grant_type_device_code_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oauth2_endpoint_token_grant_type_device_code_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    Rate limiter module and options for the device authorization flow

    The module throttles the incoming requests on `/api/oauth2/token` on the device code
    parameter.
    """

    @type oauth2_flow_device_authorization_rate_limiter ::
            {module(), Asteroid.OAuth2.DeviceAuthorization.RateLimiter.opts()}

    field(:oauth2_flow_device_authorization_rate_limiter,
      config_time: :runtime
    )

    @doc """
    Interval in seconds between 2 requests on the `/api/oauth2/token` with the same device code
    in the device authorization flow
    """

    @type oauth2_flow_device_authorization_rate_limiter_interval :: non_neg_integer()

    field(:oauth2_flow_device_authorization_rate_limiter_interval,
      config_time: :runtime,
      unit: "seconds"
    )

    @doc """
    JWT Secured Authorization Request (JAR) enabling flag

    The possible values are:
    - `:disabled`: JAR is disabled
    - `:request_only`: on ly the `"request"` parameter is enabled
    - `:request_uri_only`: only the `"request_uri"` is enabled
    - `:enabled`: both the `"request"` and `"request_uri"` parameters are enabled
    """

    @type oauth2_jar_enabled :: :disabled | :request_only | :request_uri_only | :enabled

    field(:oauth2_jar_enabled,
      config_time: :runtime
    )

    @doc """
    Plugs installed on `"/api/request_object"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_request_object_plugs :: [{module(), Keyword.t()}]

    field(:api_request_object_plugs,
      config_time: :compile
    )

    @doc """
    Defines the lifetime of a request object stored internally
    """

    @type oauth2_jar_request_object_lifetime :: non_neg_integer()

    field(:oauth2_jar_request_object_lifetime,
      config_time: :runtime,
      unit: "seconds"
    )

    @doc """
    Set the options of the HTTP request to retrieve external JAR request objects

    The options are request options of `HTTPoison.Request`
    """

    @type oauth2_jar_request_uri_get_opts :: Keyword.t()

    field(:oauth2_jar_request_uri_get_opts,
      config_time: :runtime
    )

    @doc """
    List of supported signing algorithms for JAR request objects
    """

    @type oauth2_jar_request_object_signing_alg_values_supported :: [Crypto.Key.jws_alg()]

    field(:oauth2_jar_request_object_signing_alg_values_supported,
      config_time: :runtime
    )

    @doc """
    List of supported encryption algorithms for JAR request objects
    """

    @type oauth2_jar_request_object_encryption_alg_values_supported :: [Crypto.Key.jwe_alg()]

    field(:oauth2_jar_request_object_encryption_alg_values_supported,
      config_time: :runtime
    )

    @doc """
    List of supported encryption encryption algorithms for JAR request objects
    """

    @type oauth2_jar_request_object_encryption_enc_values_supported :: [Crypto.Key.jwe_enc()]

    field(:oauth2_jar_request_object_encryption_enc_values_supported,
      config_time: :runtime
    )

    @doc """
    Determines whether the audience should be checked when the request object is signed

    Checks that the audience (one one of them) is the `"issuer"` of the server, using the
    `Asteroid.OAuth2.issuer/0` function.

    Defaults to `true`. As per the specification, there's no checking when the JWT is not
    signed.
    """

    @type oauth2_jar_request_object_verify_audience :: boolean()

    field(:oauth2_jar_request_object_verify_audience,
      config_time: :runtime
    )

    @doc """
    Determines whether the issuer should be checked when the request object is signed

    Defaults to `true`. As per the specification, there's no checking when the JWT is not
    signed.
    """

    @type oauth2_jar_request_object_verify_issuer :: boolean()

    field(:oauth2_jar_request_object_verify_issuer,
      config_time: :runtime
    )

    @doc """
    Configuration of ACRs
    """

    @type oidc_acr_config :: OIDC.ACR.config()

    field(:oidc_acr_config,
      config_time: :runtime,
      used_by: [:web_authorization_callback]
    )

    @doc """
    Scope configuration for the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_scope_config :: scope_config()

    field(:oidc_flow_authorization_code_scope_config,
      config_time: :runtime,
      used_by: [:oauth2_scope_callback]
    )

    @doc """
    Scope configuration for the OIDC implicit flow
    """

    @type oidc_flow_implicit_scope_config :: scope_config()

    field(:oidc_flow_implicit_scope_config,
      config_time: :runtime,
      used_by: [:oauth2_scope_callback]
    )

    @doc """
    Scope configuration for the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_scope_config :: scope_config()

    field(:oidc_flow_hybrid_scope_config,
      config_time: :runtime,
      used_by: [:oauth2_scope_callback]
    )

    @doc """
    Callback invoked on the `/authorize` endpoint to trigger the web authorization process flow
    for the OAuth2 authorization code flow

    This workflow is in charge of authenticating and authorizing (scopes...) the user in regards
    to the request. It will typically involve several step, i.e. display of web pages. It does
    returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the process,
    one of these callback shall be called:
    - `AsteroidWeb.AuthorizeController.authorization_granted/2`
    - `AsteroidWeb.AuthorizeController.authorization_denied/2`
    """

    @type web_authorization_callback ::
            AsteroidWeb.AuthorizeController.web_authorization_callback()

    field(:web_authorization_callback,
      config_time: :runtime,
      uses: [
        :oauth2_flow_authorization_code_web_authorization_callback,
        :oauth2_flow_implicit_web_authorization_callback,
        :oidc_acr_config,
        :oidc_flow_authorization_code_web_authorization_callback,
        :oidc_flow_implicit_web_authorization_callback,
        :oidc_flow_hybrid_web_authorization_callback
      ]
    )

    @doc """
    Callback invoked on the `/authorize` endpoint to trigger the web authorization
    process flow for the OpenID Connect authorization code flow, if the
    `:oidc_acr_config` configuration option is not used.

    This workflow is in charge of authenticating and authorizing (scopes...) the user in regards
    to the request. It will typically involve several step, i.e. display of web pages. It does
    returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the process,
    one of these callback shall be called:
    - `AsteroidWeb.AuthorizeController.authorization_granted/2`
    - `AsteroidWeb.AuthorizeController.authorization_denied/2`
    """

    @type oidc_flow_authorization_code_web_authorization_callback ::
            AsteroidWeb.AuthorizeController.web_authorization_callback()

    field(:oidc_flow_authorization_code_web_authorization_callback,
      config_time: :runtime,
      used_by: [:web_authorization_callback]
    )

    @doc """
    Callback invoked on the `/authorize` endpoint to trigger the web authorization
    process flow for the OpenID Connect implicit flow, if the
    `:oidc_acr_config` configuration option is not used.

    This workflow is in charge of authenticating and authorizing (scopes...) the user in regards
    to the request. It will typically involve several step, i.e. display of web pages. It does
    returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the process,
    one of these callback shall be called:
    - `AsteroidWeb.AuthorizeController.authorization_granted/2`
    - `AsteroidWeb.AuthorizeController.authorization_denied/2`
    """

    @type oidc_flow_implicit_web_authorization_callback ::
            AsteroidWeb.AuthorizeController.web_authorization_callback()

    field(:oidc_flow_implicit_web_authorization_callback,
      config_time: :runtime,
      used_by: [:web_authorization_callback]
    )

    @doc """
    Callback invoked on the `/authorize` endpoint to trigger the web authorization
    process flow for the OpenID Connect hybrid flow, if the
    `:oidc_acr_config` configuration option is not used.

    This workflow is in charge of authenticating and authorizing (scopes...) the user in regards
    to the request. It will typically involve several step, i.e. display of web pages. It does
    returns a `Plug.Conn.t()` to Phoenix but not to Asteroid directly. At the end of the process,
    one of these callback shall be called:
    - `AsteroidWeb.AuthorizeController.authorization_granted/2`
    - `AsteroidWeb.AuthorizeController.authorization_denied/2`
    """

    @type oidc_flow_hybrid_web_authorization_callback ::
            AsteroidWeb.AuthorizeController.web_authorization_callback()

    field(:oidc_flow_hybrid_web_authorization_callback,
      config_time: :runtime,
      used_by: [:web_authorization_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when submitting an authorization code
    in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_issue_refresh_token_init :: boolean()

    field(:oidc_flow_authorization_code_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens in the OIDC
    authorization code flow
    """

    @type oidc_flow_authorization_code_issue_refresh_token_refresh :: boolean()

    field(:oidc_flow_authorization_code_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when submitting an authorization code
    in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_issue_refresh_token_init :: boolean()

    field(:oidc_flow_hybrid_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens in the OIDC
    hybrid flow
    """

    @type oidc_flow_hybrid_issue_refresh_token_refresh :: boolean()

    field(:oidc_flow_hybrid_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines the lifetime of a refresh token in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_refresh_token_lifetime :: non_neg_integer()

    field(:oidc_flow_authorization_code_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of a refresh token in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_refresh_token_lifetime :: non_neg_integer()

    field(:oidc_flow_hybrid_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an access token in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_access_token_lifetime :: non_neg_integer()

    field(:oidc_flow_authorization_code_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an access token in the OIDC implicit flow
    """

    @type oidc_flow_implicit_access_token_lifetime :: non_neg_integer()

    field(:oidc_flow_implicit_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an access token in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_access_token_lifetime :: non_neg_integer()

    field(:oidc_flow_hybrid_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Callback called to determine the lifetime of an ID token

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oidc_id_token_lifetime_callback :: (Asteroid.Context.t() -> non_neg_integer())

    field(:oauth2_access_token_lifetime_callback,
      config_time: :runtime,
      uses: [
        :oidc_id_token_lifetime,
        :oidc_flow_authorization_code_id_token_lifetime,
        :oidc_flow_implicit_id_token_lifetime,
        :oidc_flow_hybrid_id_token_lifetime
      ]
    )

    @doc """
    Defines the lifetime of an ID token in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_id_token_lifetime :: non_neg_integer()

    field(:oidc_flow_authorization_code_id_token_lifetime,
      config_time: :runtime,
      used_by: [:oidc_id_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an ID token in the OIDC implicit flow
    """

    @type oidc_flow_implicit_id_token_lifetime :: non_neg_integer()

    field(:oidc_flow_implicit_id_token_lifetime,
      config_time: :runtime,
      used_by: [:oidc_id_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an ID token in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_id_token_lifetime :: non_neg_integer()

    field(:oidc_flow_hybrid_id_token_lifetime,
      config_time: :runtime,
      used_by: [:oidc_id_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    List of acceptable signature `alg` algorithms to sign ID tokens
    """

    @type oidc_id_token_signing_alg_values_supported :: [Crypto.Key.jws_alg()]

    field(:oidc_id_token_signing_alg_values_supported,
      config_time: :runtime
    )

    @doc """
    List of acceptable encryption `alg` algorithms to encrypt ID tokens
    """

    @type oidc_id_token_encryption_alg_values_supported :: [Crypto.Key.jwe_alg()]

    field(:oidc_id_token_encryption_alg_values_supported,
      config_time: :runtime
    )

    @doc """
    List of acceptable encryption `enc` algorithms to encrypt ID tokens
    """

    @type oidc_id_token_encryption_enc_values_supported :: [Crypto.Key.jwe_enc()]

    field(:oidc_id_token_encryption_enc_values_supported,
      config_time: :runtime
    )

    @doc """
    Callback invoked before serializing an ID token
    """

    @type token_id_token_before_serialize_callback ::
            (Asteroid.Token.IDToken.t(), Asteroid.Context.t() -> Asteroid.Token.IDToken.t())

    field(:token_id_token_before_serialize_callback,
      config_time: :runtime
    )

    @doc """
    Callback called to determine whether a new ID token should be issued when renewing
    tokens on `/token` with a refresh token grant type

    Note that client configuration takes precedence over configuration options. See
    `Asteroid.Client` fields.
    """

    @type oidc_issue_id_token_on_refresh_callback :: (Asteroid.Context.t() -> boolean())

    field(:oidc_issue_id_token_on_refresh_callback,
      config_time: :runtime,
      uses: [
        :oidc_issue_id_token_refresh,
        :oidc_flow_authorization_code_issue_id_token_refresh,
        :oidc_flow_hybrid_issue_id_token_refresh
      ]
    )

    @doc """
      Defines whether an ID token should be issued when refreshing tokens in the OIDC
      authorization code flow
    """

    @type oidc_flow_authorization_code_issue_id_token_refresh :: boolean()

    field(:oidc_flow_authorization_code_issue_id_token_refresh,
      config_time: :runtime,
      used_by: [:oidc_issue_id_token_on_refresh_callback]
    )

    @doc """
      Defines whether an ID token should be issued when refreshing tokens in the OIDC
      hybrid flow
    """

    @type oidc_flow_hybrid_issue_id_token_refresh :: boolean()

    field(:oidc_flow_hybrid_issue_id_token_refresh,
      config_time: :runtime,
      used_by: [:oidc_issue_id_token_on_refresh_callback]
    )

    @doc """
    Defines the lifetime of an authorization code in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_authorization_code_lifetime :: non_neg_integer()

    field(:oidc_flow_authorization_code_authorization_code_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_authorization_code_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an authorization code in the OIDC hybrid code flow
    """

    @type oidc_flow_hybrid_authorization_code_lifetime :: non_neg_integer()

    field(:oidc_flow_hybrid_authorization_code_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_authorization_code_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oidc_flow_authorization_code_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the serialization format of an access token in the OIDC implicit flow
    """

    @type oidc_flow_implicit_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oidc_flow_implicit_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the serialization format of an access token in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oidc_flow_hybrid_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing key name of an access token in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_access_token_signing_key :: Crypto.Key.name()

    field(:oidc_flow_authorization_code_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing key name of an access token in the OIDC implicit flow
    """

    @type oidc_flow_implicit_access_token_signing_key :: Crypto.Key.name()

    field(:oidc_flow_implicit_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing key name of an access token in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_access_token_signing_key :: Crypto.Key.name()

    field(:oidc_flow_hybrid_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the OIDC authorization code flow
    """

    @type oidc_flow_authorization_code_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oidc_flow_authorization_code_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the OIDC implicit flow
    """

    @type oidc_flow_implicit_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oidc_flow_implicit_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Defines the signing algorithm of an access token in the OIDC hybrid flow
    """

    @type oidc_flow_hybrid_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oidc_flow_hybrid_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Plugs installed on `"/api/ooidc"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oidc_plugs :: [{module(), Keyword.t()}]

    field(:api_oidc_plugs,
      config_time: :compile
    )

    @doc """
    Plugs installed on `"/api/oidc/userinfo"`

    See also [protecting APIs](protecting-apis.html)
    """

    @type api_oidc_endpoint_userinfo_plugs :: [{module(), Keyword.t()}]

    field(:api_oidc_endpoint_userinfo_plugs,
      config_time: :compile
    )

    @doc """
    Callback invoked on the json response on the `/userinfo` endpoint
    """

    @type oidc_endpoint_userinfo_before_send_resp_callback ::
            (map(), Asteroid.Context.t() -> map())

    field(:oidc_endpoint_userinfo_before_send_resp_callback,
      config_time: :runtime
    )

    @doc """
    Callback invoked on the `t:Plug.Conn.t/0` response on the `/userinfo` endpoint
    """

    @type oidc_endpoint_userinfo_before_send_conn_callback ::
            (Plug.Conn.t(), Asteroid.Context.t() -> Plug.Conn.t())

    field(:oidc_endpoint_userinfo_before_send_conn_callback,
      config_time: :runtime
    )

    @doc """
    List of acceptable signature `alg` algorithms for the signature response on the
    `/api/oidc/userinfo` endpoint
    """

    @type oidc_endpoint_userinfo_signature_alg_values_supported :: [Crypto.Key.jws_alg()]

    field(:oidc_endpoint_userinfo_signature_alg_values_supported,
      config_time: :runtime
    )

    @doc """
    List of acceptable encryption `alg` algorithms for the encrypted response on the
    `/api/oidc/userinfo` endpoint
    """

    @type oidc_endpoint_userinfo_encryption_alg_values_supported :: [Crypto.Key.jwe_alg()]

    field(:oidc_endpoint_userinfo_encryption_alg_values_supported,
      config_time: :runtime
    )

    @doc """
    List of acceptable encryption `enc` algorithms for the encrypted response on the
    `/api/oidc/userinfo` endpoint
    """

    @type oidc_endpoint_userinfo_encryption_enc_values_supported :: [Crypto.Key.jwe_enc()]

    field(:oidc_endpoint_userinfo_encryption_enc_values_supported,
      config_time: :runtime
    )

    @doc """
    Claims supported (declarative)

    This is only used for publishing it on the discovery endpoint.
    """

    @type oidc_claims_supported :: [OIDC.claim_name()]

    field(:oidc_claims_supported,
      config_time: :runtime
    )

    @doc """
    Policy for response mode selection

    3 values are possible:
    - `:disabled`: the `"response_mode"` param is not processed, and the default response
    mode for the flow is choosen
    - `:oidc_only`: the `"response_mode"` param is used for OIDC flows only
    - `:enabled`: the `"response_mode"` param is used for all flows
    """

    @type oauth2_response_mode_policy :: :disabled | :oidc_only | :enabled

    field(:oauth2_response_mode_policy,
      config_time: :runtime
    )

    @doc """
    Callback invoked to calculate the `"sub"` returned in OpenID Connect ID tokens and
    on the `/userinfo` endpoint
    """

    @type oidc_subject_identifier_callback ::
            (Subject.t(), Client.t() -> String.t())

    field(:oidc_subject_identifier_callback,
      config_time: :runtime
    )

    @doc """
    Salt for the pairwise subject identifier type

    By default, a random value set at compile time, which means that the pairwise `"subs"`
    **will change** when compiling again, and **won't be stable**. To have stability, change
    this configuration option with a random value (which doesn't need to be particularly
    protected against theft), for example generating it with the following command:

    ```elixir
    $ mix phx.gen.secret
    <KEY>
    ```
    """

    @type oidc_subject_identifier_pairwise_salt :: String.t()

    field(:oidc_subject_identifier_pairwise_salt,
      config_time: :runtime
    )

    @doc """
    OIDC display values supported

    Voluntary information to be published on the metadata endpoint. It is not used otherwise.
    """

    @type oidc_endpoint_metadata_display_values_supported :: [String.t()]

    field(:oidc_endpoint_metadata_display_values_supported,
      config_time: :runtime
    )

    @doc """
    Defines the lifetime of an access token
    """

    @type oauth2_access_token_lifetime :: non_neg_integer()

    field(:oauth2_access_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_access_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the serialization format of an access token
    """

    @type oauth2_access_token_serialization_format ::
            Asteroid.Token.serialization_format()

    field(:oauth2_access_token_serialization_format,
      config_time: :runtime,
      used_by: [:oauth2_access_token_serialization_format_callback]
    )

    @doc """
    Defines the signing algorithm of an access token
    """

    @type oauth2_access_token_signing_alg :: Crypto.Key.jws_alg()

    field(:oauth2_access_token_signing_alg,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_alg_callback]
    )

    @doc """
    Defines the signing key name of an access token
    """

    @type oauth2_access_token_signing_key :: Crypto.Key.name()

    field(:oauth2_access_token_signing_key,
      config_time: :runtime,
      used_by: [:oauth2_access_token_signing_key_callback]
    )

    @doc """
    Defines the lifetime of an authorization code
    """

    @type oauth2_authorization_code_lifetime :: non_neg_integer()

    field(:oauth2_authorization_code_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_authorization_code_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines whether a refresh token should be issued
    """

    @type oauth2_issue_refresh_token_init :: boolean()

    field(:oauth2_issue_refresh_token_init,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines whether a refresh token should be issued when refreshing tokens
    """

    @type oauth2_issue_refresh_token_refresh :: boolean()

    field(:oauth2_issue_refresh_token_refresh,
      config_time: :runtime,
      used_by: [:oauth2_issue_refresh_token_callback]
    )

    @doc """
    Defines the lifetime of a refresh token
    """

    @type oauth2_refresh_token_lifetime :: non_neg_integer()

    field(:oauth2_refresh_token_lifetime,
      config_time: :runtime,
      used_by: [:oauth2_refresh_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines the lifetime of an ID token
    """

    @type oidc_id_token_lifetime :: non_neg_integer()

    field(:oidc_id_token_lifetime,
      config_time: :runtime,
      used_by: [:oidc_id_token_lifetime_callback],
      unit: "seconds"
    )

    @doc """
    Defines whether an ID token should be issued when refreshing tokens
    """

    @type oidc_issue_id_token_refresh :: boolean()

    field(:oidc_issue_id_token_refresh,
      config_time: :runtime,
      used_by: [:oidc_issue_id_token_on_refresh_callback]
    )

    ### end of configuration options
  end

  @doc """
  Returns the markdown link to the documentation of a configuration option
  """

  @spec link_to_option(atom()) :: String.t()

  def link_to_option(configuration_option) do
    configuration_option = to_string(configuration_option)

    "[`:#{configuration_option}`](Asteroid.Config.html#module-#{configuration_option})"
  end
end


<|start_filename|>lib/asteroid/oauth2/device_authorization.ex<|end_filename|>
defmodule Asteroid.OAuth2.DeviceAuthorization do
  @moduledoc """
  Types and convenience functions to work with the device flow
  """

  import Asteroid.Utils

  defmodule ExpiredTokenError do
    @moduledoc """
    Error returned when a device code has expired
    """

    defexception []

    @type t :: %__MODULE__{}

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "The device code has expired"

        :normal ->
          "The device code has expired"

        :minimal ->
          ""
      end
    end
  end

  defmodule AuthorizationPendingError do
    @moduledoc """
    Error returned when a device code is valid but has not been granted access yet by the user
    """

    defexception []

    @type t :: %__MODULE__{}

    def message(_) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          "The device code authorization is pending"

        :normal ->
          "The device code authorization is pending"

        :minimal ->
          ""
      end
    end
  end

  defmodule RateLimitedError do
    @moduledoc """
    Error returned when requests with a device code become rate-limited

    The `:retry_after` option is in seconds.
    """

    defexception [:retry_after]

    @type t :: %__MODULE__{
            retry_after: non_neg_integer()
          }

    def message(%{retry_after: retry_after}) do
      case astrenv(:api_error_response_verbosity) do
        :debug ->
          if is_integer(retry_after) do
            "Too many requests (retry after #{to_string(retry_after)} seconds)"
          else
            "Too many requests"
          end

        :normal ->
          "Too many requests"

        :minimal ->
          ""
      end
    end
  end

  alias Asteroid.Context

  @type user_code :: String.t()

  @type device_code :: String.t()

  @doc """
  8-character user code generation fucntion

  This function returns the characters of the follwoing alphabet:
  "ABCDEFGHIJKLMNPQRSTUVWXYZ2345678" with an entropy of 32^8
  """

  @spec user_code(Context.t()) :: String.t()

  def user_code(_) do
    :crypto.strong_rand_bytes(5)
    |> Base.encode32(padding: false)
    |> String.replace("O", "8")
  end

  @doc """
  Returns `:ok` if the device code is not rate-limited,
  `{:error, %Asteroid.OAuth2.DeviceAuthorization.RateLimitedError{}}` otherwise.
  """

  @spec rate_limited?(device_code) :: :ok | {:error, RateLimitedError.t()}

  def rate_limited?(device_code) do
    case astrenv(:oauth2_flow_device_authorization_rate_limiter) do
      {module, opts} ->
        case module.check(device_code, opts) do
          :ok ->
            :ok

          {:rate_limited, nil} ->
            {:error, RateLimitedError.exception([])}

          {:rate_limited, retry_after} ->
            {:error, RateLimitedError.exception(retry_after: retry_after)}
        end

      nil ->
        :ok
    end
  end
end


<|start_filename|>test/asteroid_web/views/page_view_test.exs<|end_filename|>
defmodule AsteroidWeb.PageViewTest do
  use AsteroidWeb.ConnCase, async: true
end
