Skip to main content

lib/ash_authentication_phoenix/oauth2_server/router.ex

# SPDX-FileCopyrightText: 2026 ash_authentication_oauth2_server contributors <https://github.com/ash-project/ash_authentication_oauth2_server/graphs/contributors>
#
# SPDX-License-Identifier: MIT

defmodule AshAuthentication.Phoenix.Oauth2Server.Router do
  @moduledoc """
  Phoenix router macros for mounting the OAuth 2.1 authorization server.

  `use` this module inside your router to gain access to
  `oauth2_server_consent_routes/1` (browser-facing, user consent) and
  `oauth2_server_protocol_routes/1` (client-facing protocol endpoints).

  ## Example

      defmodule MyAppWeb.Router do
        use Phoenix.Router
        use AshAuthentication.Phoenix.Oauth2Server.Router

        scope "/" do
          pipe_through :browser
          oauth2_server_consent_routes oauth2_server: MyApp.Oauth2Server
        end

        scope "/" do
          pipe_through :api
          oauth2_server_protocol_routes oauth2_server: MyApp.Oauth2Server
        end
      end

  The two macros forward to:

    * `AshAuthentication.Phoenix.Oauth2Server.ConsentRouter` — handles
      `/oauth/authorize` (the user-driven consent step).
    * `AshAuthentication.Phoenix.Oauth2Server.ProtocolRouter` — handles
      `/oauth/register`, `/oauth/token`, `/oauth/revoke`, and the three
      metadata documents under `/.well-known`.
  """

  defmacro __using__(_opts) do
    quote do
      import AshAuthentication.Phoenix.Oauth2Server.Router,
        only: [
          oauth2_server_consent_routes: 0,
          oauth2_server_consent_routes: 1,
          oauth2_server_protocol_routes: 0,
          oauth2_server_protocol_routes: 1
        ]
    end
  end

  @doc """
  Generate the routes for the user-driven consent step of an OAuth 2.1
  authorization-server flow.

  Mount this inside a scope that pipes through your **browser** pipeline
  (with `:protect_from_forgery` and session loading) — both the consent
  GET and POST need a logged-in user and CSRF protection.

  ## Example

      scope "/" do
        pipe_through :browser
        oauth2_server_consent_routes oauth2_server: MyApp.Oauth2Server
      end

  ## Options

    * `:oauth2_server` (required) — your `Oauth2Server` config module.
    * `:path` — base path. Defaults to `/oauth/authorize`.
    * `:consent_view` — module exposing `render(:consent, assigns)`.
      Defaults to `AshAuthentication.Phoenix.Oauth2Server.ConsentView`.
  """
  defmacro oauth2_server_consent_routes(opts \\ []) when is_list(opts) do
    quote location: :keep do
      opts = unquote(opts)
      server = Keyword.fetch!(opts, :oauth2_server)
      path = Keyword.get(opts, :path, "/oauth/authorize")

      consent_view =
        Keyword.get(opts, :consent_view, AshAuthentication.Phoenix.Oauth2Server.ConsentView)

      scope "/", alias: false do
        forward path, AshAuthentication.Phoenix.Oauth2Server.ConsentRouter,
          oauth2_server: server,
          consent_view: consent_view
      end
    end
  end

  @doc """
  Generate the routes for the client-facing OAuth 2.1 protocol endpoints —
  discovery, dynamic client registration, token, and revocation.

  Mount this inside a scope that pipes through your **API** pipeline. These
  endpoints are called by external OAuth clients without a browser session,
  so CSRF must NOT apply.

  ## Example

      scope "/" do
        pipe_through :api
        oauth2_server_protocol_routes oauth2_server: MyApp.Oauth2Server
      end

  ## Options

    * `:oauth2_server` (required) — your `Oauth2Server` config module.
    * `:oauth_path` — prefix for `/token`, `/register`, etc. Defaults to `/oauth`.
    * `:well_known_path` — prefix for `/oauth-authorization-server`,
      `/oauth-protected-resource`, `/openid-configuration`.
      Defaults to `/.well-known`.
  """
  defmacro oauth2_server_protocol_routes(opts \\ []) when is_list(opts) do
    quote location: :keep do
      opts = unquote(opts)
      server = Keyword.fetch!(opts, :oauth2_server)
      oauth_path = Keyword.get(opts, :oauth_path, "/oauth")
      well_known_path = Keyword.get(opts, :well_known_path, "/.well-known")

      scope "/", alias: false do
        forward oauth_path, AshAuthentication.Phoenix.Oauth2Server.ProtocolRouter,
          oauth2_server: server

        forward well_known_path, AshAuthentication.Phoenix.Oauth2Server.ProtocolRouter,
          oauth2_server: server
      end
    end
  end
end