lib/openagents_web/plugs/api_token_auth.ex

main at 58e6347eeb72 · 1 KB

defmodule OpenAgentsWeb.Plugs.ApiTokenAuth do
  @moduledoc "Authenticates a scoped first-party bearer credential for JSON APIs."

  import Plug.Conn

  alias OpenAgents.ApiTokens

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

  def call(conn, required_scope) do
    with {:ok, plaintext} <- bearer(conn),
         {:ok, user, token} <- ApiTokens.authenticate(plaintext, required_scope) do
      conn
      |> put_resp_header("cache-control", "no-store")
      |> assign(:current_user, user)
      |> assign(:api_token, token)
      |> assign(:api_scope, required_scope)
    else
      _denied -> refuse(conn)
    end
  end

  defp bearer(conn) do
    case get_req_header(conn, "authorization") do
      ["Bearer " <> token] when token != "" -> {:ok, token}
      _missing_or_ambiguous -> {:error, :missing_api_token}
    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