lib/openagents_web/plugs/optional_api_token_auth.ex

main at 58e6347eeb72 · 1 KB

defmodule OpenAgentsWeb.Plugs.OptionalApiTokenAuth do
  @moduledoc "Authenticates a supplied first-party bearer credential and permits anonymous requests."

  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
      [] ->
        assign(conn, :current_user, nil)

      ["Bearer " <> token] when token != "" ->
        authenticate(conn, token, required_scope)

      _invalid ->
        refuse(conn)
    end
  end

  defp authenticate(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} ->
        refuse(conn)
    end
  end

  # A 401 from this pipeline is the first refusal an issue-family caller can
  # meet, so it carries the same envelope the controllers behind it use. The
  # `error` key predates the envelope and every measured client reads it, so it
  # rides beside the envelope rather than being replaced.
  defp refuse(conn) do
    body =
      OpenAgentsWeb.ApiError.envelope(conn, "unauthenticated",
        message: "Requires an API token with the scope this route needs",
        legacy: %{"error" => "invalid_api_token"}
      )

    conn
    |> put_status(:unauthorized)
    |> put_resp_header("cache-control", "no-store")
    |> Phoenix.Controller.json(body)
    |> halt()
  end
end