lib/openagents_web/plugs/ambient_api_token_auth.ex

58e6347eeb72 · 2 KB

defmodule OpenAgentsWeb.Plugs.AmbientApiTokenAuth do
  @moduledoc """
  Recognizes an account when one presents a valid credential, and refuses
  nobody.

  `OpenAgentsWeb.Plugs.OptionalApiTokenAuth` widens what an anonymous caller
  may *read*, so a credential it cannot verify is an error worth a `401`. This
  plug does something different: it grants no authority at all. It only lets a
  route that already answers anonymously know whose account is on the other
  end, so the route can add what that account is owed — its memories, on
  `POST /api/v1/responses` — and answer exactly as before when it cannot tell.

  Because nothing here is granted, nothing here is refused. A malformed header,
  an expired token, a token scoped for something else: each leaves
  `:current_user` `nil` and the request proceeds as the anonymous request it
  already was. Refusing instead would break every caller that reaches an
  anonymous route with an unrelated `Authorization` header — which is a live
  shape on this endpoint, and which was working before recall existed.

  A route behind this plug must therefore treat `:current_user` as a
  convenience and never as authorization. If a route needs authority, it
  belongs on `ApiTokenAuth` instead.
  """

  import Plug.Conn

  alias OpenAgents.ApiTokens

  def init(options), do: Keyword.fetch!(options, :scope)

  def call(conn, required_scope) do
    case get_req_header(conn, "authorization") do
      ["Bearer " <> token] when token != "" -> recognize(conn, token, required_scope)
      _absent_or_unreadable -> anonymous(conn)
    end
  end

  defp recognize(conn, token, required_scope) do
    case ApiTokens.authenticate(token, required_scope) do
      {:ok, user, api_token} ->
        conn
        |> put_resp_header("cache-control", "no-store")
        |> assign(:current_user, user)
        |> assign(:api_token, api_token)
        |> assign(:api_scope, required_scope)

      {:error, :invalid_api_token} ->
        anonymous(conn)
    end
  end

  defp anonymous(conn) do
    conn
    |> assign(:current_user, nil)
    |> assign(:api_token, nil)
    |> assign(:api_scope, nil)
  end
end