lib/openagents_web/controllers/device_authorization_controller.ex

main at 58e6347eeb72 · 2 KB

defmodule OpenAgentsWeb.DeviceAuthorizationController do
  @moduledoc "Creates and polls short-lived CLI browser authorizations."

  use OpenAgentsWeb, :controller

  alias OpenAgents.{ApiTokens, DeviceAuthorizations}

  def create(conn, params) do
    case DeviceAuthorizations.create(requested_scopes(params), params["device_name"]) do
      {:ok, authorization, device_code, user_code} ->
        verification_uri = OpenAgentsWeb.Endpoint.url() <> "/device"

        conn
        |> put_status(:created)
        |> no_store()
        |> json(%{
          "device_code" => device_code,
          "user_code" => user_code,
          "verification_uri" => verification_uri,
          "verification_uri_complete" =>
            verification_uri <> "?" <> URI.encode_query(%{"user_code" => user_code}),
          "expires_in" => DateTime.diff(authorization.expires_at, DateTime.utc_now(), :second),
          "interval" => authorization.interval_seconds,
          "scope" => Enum.join(authorization.scopes, " ")
        })

      {:error, %Ecto.Changeset{}} ->
        error(conn, :bad_request, "invalid_scope")

      {:error, :invalid_scopes} ->
        error(conn, :bad_request, "invalid_scope")

      {:error, _reason} ->
        error(conn, :service_unavailable, "authorization_unavailable")
    end
  end

  # A device authorization may request any real scope, including the
  # operator-only one. Approval, not this parameter, decides whether the
  # requester may hold it.
  defp requested_scopes(params) do
    case params["scope"] || params["scopes"] do
      scope when is_binary(scope) -> String.split(scope, " ", trim: true)
      scopes when is_list(scopes) -> Enum.filter(scopes, &(&1 in ApiTokens.allowed_scopes()))
      _absent -> ApiTokens.default_scopes()
    end
  end

  def token(conn, %{"device_code" => device_code}) do
    case DeviceAuthorizations.poll(device_code) do
      {:ok, plaintext, api_token} ->
        conn
        |> no_store()
        |> json(%{
          "access_token" => plaintext,
          "token_type" => "Bearer",
          "scope" => Enum.join(api_token.scopes, " "),
          "expires_in" => DateTime.diff(api_token.expires_at, DateTime.utc_now(), :second)
        })

      {:error, :authorization_pending} ->
        error(conn, :precondition_required, "authorization_pending")

      {:error, :slow_down} ->
        error(conn, :too_many_requests, "slow_down")

      {:error, :authorization_unavailable} ->
        error(conn, :service_unavailable, "authorization_unavailable")

      {:error, _reason} ->
        error(conn, :bad_request, "access_denied")
    end
  end

  def token(conn, _params), do: error(conn, :bad_request, "access_denied")

  defp error(conn, status, code) do
    conn
    |> put_status(status)
    |> no_store()
    |> json(%{"code" => code})
  end

  defp no_store(conn), do: put_resp_header(conn, "cache-control", "no-store")
end