| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- //! Authorization-server metadata (RFC 8414).
- //!
- //! `GET /.well-known/oauth-authorization-server` returns the AS document. The
- //! only server-state-dependent member is `scopes_supported`: it is read from
- //! the enabled `scopes` table (the single source of truth, populated at runtime
- //! via `config sync` / `scope create`) on every request. The catalog is mutated
- //! out of process by the CLI, so an in-process cache could not be invalidated
- //! across replicas; a per-request read makes a mutation visible immediately with
- //! no restart. When Postgres is unavailable the endpoint fails closed with `503
- //! temporarily_unavailable` rather than advertising an unknown scope list.
- //!
- //! No `jwks_uri`: the AS signs nothing (there is no AS JWT-signing key), and
- //! `code_challenge_methods_supported` advertises `S256` only — `plain` is
- //! never offered.
- use axum::{Json, extract::State};
- use serde::Serialize;
- use crate::{db::scope_repo, error::AppError, state::AppState};
- /// The five grant types this server supports (RFC 6749, RFC 8628, RFC 8693).
- pub const GRANT_TYPES_SUPPORTED: [&str; 5] = [
- "authorization_code",
- "refresh_token",
- "client_credentials",
- "urn:ietf:params:oauth:grant-type:device_code",
- "urn:ietf:params:oauth:grant-type:token-exchange",
- ];
- /// Client-authentication methods accepted at the token endpoint.
- ///
- /// `none` covers public clients; `tls_client_auth` and
- /// `self_signed_tls_client_auth` are the RFC 8705 mTLS methods.
- pub const TOKEN_AUTH_METHODS: [&str; 6] = [
- "client_secret_basic",
- "client_secret_post",
- "none",
- "private_key_jwt",
- "tls_client_auth",
- "self_signed_tls_client_auth",
- ];
- /// Methods accepted at the revocation endpoint (RFC 7009). Public clients may
- /// revoke their own tokens, so `none` is included.
- pub const REVOCATION_AUTH_METHODS: [&str; 6] = TOKEN_AUTH_METHODS;
- /// Methods accepted at the introspection endpoint (RFC 7662). Introspection
- /// requires an authenticated caller, so `none` is not offered.
- pub const INTROSPECTION_AUTH_METHODS: [&str; 5] = [
- "client_secret_basic",
- "client_secret_post",
- "private_key_jwt",
- "tls_client_auth",
- "self_signed_tls_client_auth",
- ];
- /// JWS algorithms accepted for `private_key_jwt` client assertions
- /// (`token_endpoint_auth_signing_alg_values_supported`); kept in sync with
- /// `protocol::client_auth::assertion::ALLOWED_ALGS`.
- pub const CLIENT_ASSERTION_SIGNING_ALG_VALUES: [&str; 4] = ["ES256", "EdDSA", "RS256", "PS256"];
- /// JWS algorithms accepted for DPoP proofs (`dpop_signing_alg_values_supported`).
- pub const DPOP_SIGNING_ALG_VALUES: [&str; 3] = ["ES256", "EdDSA", "RS256"];
- /// RFC 8414 authorization-server metadata document.
- #[derive(Debug, Serialize)]
- pub struct AuthorizationServerMetadata {
- /// Public issuer identifier.
- pub issuer: String,
- /// Authorization endpoint.
- pub authorization_endpoint: String,
- /// Token endpoint.
- pub token_endpoint: String,
- /// Dynamic client-registration endpoint.
- pub registration_endpoint: String,
- /// Token-introspection endpoint.
- pub introspection_endpoint: String,
- /// Token-revocation endpoint.
- pub revocation_endpoint: String,
- /// Pushed authorization request endpoint.
- pub pushed_authorization_request_endpoint: String,
- /// Device authorization endpoint.
- pub device_authorization_endpoint: String,
- /// Response types supported (code only).
- pub response_types_supported: [&'static str; 1],
- /// Grant types supported.
- pub grant_types_supported: [&'static str; 5],
- /// Token-endpoint client-authentication methods.
- pub token_endpoint_auth_methods_supported: [&'static str; 6],
- /// Signing algorithms for `private_key_jwt` client assertions.
- pub token_endpoint_auth_signing_alg_values_supported: [&'static str; 4],
- /// Introspection-endpoint client-authentication methods.
- pub introspection_endpoint_auth_methods_supported: [&'static str; 5],
- /// Revocation-endpoint client-authentication methods.
- pub revocation_endpoint_auth_methods_supported: [&'static str; 6],
- /// PKCE code-challenge methods (`S256` only; `plain` is never offered).
- pub code_challenge_methods_supported: [&'static str; 1],
- /// DPoP proof signing algorithms.
- pub dpop_signing_alg_values_supported: [&'static str; 3],
- /// Whether mTLS-bound access tokens are supported (RFC 8705).
- pub tls_client_certificate_bound_access_tokens: bool,
- /// Whether the `iss` authorization-response parameter is supported (RFC 9207).
- pub authorization_response_iss_parameter_supported: bool,
- /// Enabled scope catalog (single source of truth: the `scopes` table).
- pub scopes_supported: Vec<String>,
- }
- impl AuthorizationServerMetadata {
- /// Build the document for `issuer` with the given enabled scope catalog.
- ///
- /// `issuer` is validated elsewhere to be absolute `https` with no trailing
- /// slash, so endpoint paths append directly.
- #[must_use]
- pub fn for_issuer(issuer: &str, scopes_supported: Vec<String>) -> Self {
- Self {
- issuer: issuer.to_owned(),
- authorization_endpoint: endpoint(issuer, "/authorize"),
- token_endpoint: endpoint(issuer, "/token"),
- registration_endpoint: endpoint(issuer, "/register"),
- introspection_endpoint: endpoint(issuer, "/introspect"),
- revocation_endpoint: endpoint(issuer, "/revoke"),
- pushed_authorization_request_endpoint: endpoint(issuer, "/par"),
- device_authorization_endpoint: endpoint(issuer, "/device_authorization"),
- response_types_supported: ["code"],
- grant_types_supported: GRANT_TYPES_SUPPORTED,
- token_endpoint_auth_methods_supported: TOKEN_AUTH_METHODS,
- token_endpoint_auth_signing_alg_values_supported: CLIENT_ASSERTION_SIGNING_ALG_VALUES,
- introspection_endpoint_auth_methods_supported: INTROSPECTION_AUTH_METHODS,
- revocation_endpoint_auth_methods_supported: REVOCATION_AUTH_METHODS,
- code_challenge_methods_supported: ["S256"],
- dpop_signing_alg_values_supported: DPOP_SIGNING_ALG_VALUES,
- tls_client_certificate_bound_access_tokens: true,
- authorization_response_iss_parameter_supported: true,
- scopes_supported,
- }
- }
- }
- /// Append an endpoint path to the (trailing-slash-free) issuer.
- fn endpoint(issuer: &str, path: &str) -> String {
- format!("{issuer}{path}")
- }
- /// `GET /.well-known/oauth-authorization-server`.
- ///
- /// # Errors
- /// Returns `503 temporarily_unavailable` when Postgres is unreachable and the
- /// enabled scope list cannot be read.
- pub async fn metadata(
- State(state): State<AppState>,
- ) -> Result<Json<AuthorizationServerMetadata>, AppError> {
- let rows = scope_repo::list_enabled(&state.pool).await?;
- let mut scopes: Vec<String> = rows.into_iter().map(|row| row.name).collect();
- scopes.sort();
- Ok(Json(AuthorizationServerMetadata::for_issuer(
- &state.config.issuer,
- scopes,
- )))
- }
- #[cfg(test)]
- mod tests;
|