Skip to main content

lib/ash_authentication/oauth2_server/metadata.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.Oauth2Server.Metadata do
  @moduledoc """
  Builders for the discovery metadata endpoints.

    * `protected_resource/1` (RFC 9728) — for the resource server, served at
      `/.well-known/oauth-protected-resource`.
    * `authorization_server/1` (RFC 8414) — for the authorization server,
      served at `/.well-known/oauth-authorization-server`.

  Both return plain maps; controllers JSON-encode them.
  """

  @doc """
  Build the OAuth Protected Resource Metadata document (RFC 9728).

  `context` is forwarded to the server's `resource_url/1` and `issuer_url/1`
  callbacks so per-request (e.g. per-tenant) resolution works. Single-tenant
  callers can pass `%{}`.
  """
  @spec protected_resource(server :: module(), context :: map()) :: map()
  def protected_resource(server, context \\ %{}) do
    %{
      "resource" => server.resource_url(context),
      "authorization_servers" => [server.issuer_url(context)],
      "scopes_supported" => server.scopes(),
      "bearer_methods_supported" => ["header"]
    }
  end

  @doc """
  Build the OAuth Authorization Server Metadata document (RFC 8414).

  Endpoint paths are derived from the `issuer_url` so that mounting under a
  custom prefix works without configuration. `context` is forwarded to
  `issuer_url/1` so per-tenant deployments can resolve the issuer from the
  current request.
  """
  @spec authorization_server(server :: module(), context :: map()) :: map()
  def authorization_server(server, context \\ %{}) do
    issuer = server.issuer_url(context)

    base = %{
      "issuer" => issuer,
      "authorization_endpoint" => issuer <> "/oauth/authorize",
      "token_endpoint" => issuer <> "/oauth/token",
      "revocation_endpoint" => issuer <> "/oauth/revoke",
      "response_types_supported" => ["code"],
      "grant_types_supported" => ["authorization_code", "refresh_token"],
      "code_challenge_methods_supported" => ["S256"],
      "token_endpoint_auth_methods_supported" => ["none"],
      "scopes_supported" => server.scopes()
    }

    # Only advertise the DCR endpoint when it's actually enabled.
    # Clients use this field to decide whether to attempt registration.
    if server.dcr_enabled?(),
      do: Map.put(base, "registration_endpoint", issuer <> "/oauth/register"),
      else: base
  end
end