Skip to main content

alibi_plugins/
oauth_provider.rs

1//! OAuth authorization-server discovery from installed issuer and provider policy.
2pub use authentication::{OAuthAuthenticatedClient, OAuthClientAuthenticationOptions};
3mod access_validation;
4mod authentication;
5pub use client_secret::{OAuthClientSecretCipher, OAuthClientSecretHasher};
6mod assertion;
7mod authorization;
8mod claims_request;
9mod client_jwks;
10mod client_secret;
11mod clients;
12mod metadata_document;
13pub use metadata_document::{
14    OAuthClientMetadata, OAuthClientRegistrationResult, RegisterClientMetadataDocumentInput,
15};
16mod consent;
17mod continuation;
18mod credentials;
19mod discovery;
20mod dpop;
21mod extension_authentication;
22mod interaction;
23pub use extension_authentication::{
24    OAuthClientAssertionInput, OAuthClientAuthenticationStrategy,
25    OAuthExtensionClientAuthentication, OAuthVerifiedClientAssertion,
26};
27mod extension_claims;
28pub use extension_claims::{
29    OAuthExtensionClaims, OAuthTokenClaimsContributor, OAuthTokenClaimsInput,
30    OAuthUserInfoClaimsContributor, OAuthUserInfoClaimsInput,
31};
32mod extensions;
33pub use extensions::{
34    OAuthExtensionGrantHandler, OAuthExtensionGrantInput, OAuthMetadataContributor,
35    OAuthMetadataExtensionInput, OAuthMetadataType, OAuthProviderApi, OAuthProviderExtension,
36};
37mod id_token;
38mod issuance;
39pub use issuance::{OAuthTokenGenerator, OAuthTokenIssueParams};
40mod initial_access_token;
41mod introspection;
42pub use initial_access_token::{
43    OAuthInitialAccessTokenAuthorization, OAuthInitialAccessTokenError,
44    OAuthInitialAccessTokenInput, OAuthInitialAccessTokenValidator,
45};
46mod logout;
47mod machine;
48mod post_login;
49mod rate_limit;
50mod refresh;
51pub use rate_limit::{OAuthEndpointRateLimit, OAuthProviderRateLimits};
52mod refresh_format;
53pub use refresh_format::{
54    OAuthDecodedRefreshToken, OAuthRefreshTokenEncryption, OAuthRefreshTokenFormatter,
55};
56mod resource_cache;
57mod resource_policy;
58mod resource_seed;
59pub use resource_seed::{OAuthResourceIdentifierValidator, OAuthResourceSeedMode};
60mod resources;
61mod revocation;
62mod select_account;
63mod session_retirement;
64mod signup;
65pub use post_login::{
66    OAuthConsentReference, OAuthConsentReferenceContext, OAuthInteractionContext,
67    OAuthPostLoginConfig, OAuthPostLoginContext, OAuthPostLoginPolicy,
68};
69pub use select_account::{
70    OAuthSelectAccountConfig, OAuthSelectAccountContext, OAuthSelectAccountPolicy,
71};
72pub use signup::{OAuthSignupConfig, OAuthSignupContext, OAuthSignupPolicy, OAuthSignupRedirect};
73mod subject;
74mod token;
75mod token_storage;
76pub use token_storage::OAuthTokenHasher;
77mod userinfo;
78use alibi_core::endpoint::{
79    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
80};
81use alibi_core::{
82    AuthContext, AuthError, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
83    AuthSchema, HttpMethod,
84};
85pub use assertion::consume_client_assertion;
86use async_trait::async_trait;
87pub use client_jwks::{
88    OAuthClientMetadataAbortSignal, OAuthClientMetadataFetchOptions,
89    OAuthClientMetadataFetchResponse, OAuthClientMetadataResourceFetch,
90};
91pub use clients::{CreateOAuthClientInput, RegisterOAuthClientInput};
92pub use discovery::OAuthClientDiscovery;
93use serde_json::{Value, json};
94
95/// Actual authenticated context passed to an operator client-privilege decision.
96/// The pinned hook receives no client or reference identifier.
97#[derive(Debug, Clone)]
98pub struct OAuthClientPrivilegeContext {
99    pub action: &'static str,
100    pub headers: alibi_core::Headers,
101    pub user: Value,
102    pub session: Value,
103}
104#[async_trait]
105pub trait OAuthClientPrivileges: Send + Sync + std::fmt::Debug {
106    async fn allowed(&self, context: &OAuthClientPrivilegeContext) -> AuthResult<bool>;
107}
108/// Authenticated operator input for resource catalog and client-resource link management.
109#[derive(Debug, Clone)]
110pub struct OAuthResourcePrivilegeContext {
111    pub action: &'static str,
112    pub headers: alibi_core::Headers,
113    pub user: Value,
114    pub session: Value,
115    /// Decoded resource identifier; absent for create and list. No client identity is supplied.
116    pub resource_id: Option<String>,
117}
118#[async_trait]
119pub trait OAuthResourcePrivileges: Send + Sync + std::fmt::Debug {
120    async fn allowed(&self, context: &OAuthResourcePrivilegeContext) -> AuthResult<bool>;
121}
122/// Application-owned client identifiers. Empty output is preserved, matching the reference.
123pub trait OAuthClientIdGenerator: Send + Sync + std::fmt::Debug {
124    fn generate(&self) -> AuthResult<String>;
125}
126/// Application-owned client credential generation. Empty output uses the default generator.
127pub trait OAuthClientSecretGenerator: Send + Sync + std::fmt::Debug {
128    fn generate(&self) -> AuthResult<String>;
129}
130/// Application redirect trust decision. URI safety remains provider-owned.
131///
132/// The URI is raw effective input on schema errors (the original URL query on
133/// request-object or URI-resolution errors), and schema-parsed input otherwise.
134/// Neither argument uses parsed URL equality. Errors deny trust rather than failing the route.
135#[async_trait]
136pub trait OAuthRedirectUriValidator: Send + Sync + std::fmt::Debug {
137    async fn validate(
138        &self,
139        redirect_uri: &str,
140        registered_uris: &[String],
141        default_result: bool,
142    ) -> AuthResult<bool>;
143}
144/// The original request URI and URL-supplied client identity, before schema parsing.
145pub struct OAuthRequestUriInput<'a> {
146    pub request_uri: &'a str,
147    pub client_id: &'a str,
148    /// Initialized application context, original logical query and genuine physical request.
149    pub context: &'a alibi_core::CallbackContext,
150}
151/// Authorization parameters with JavaScript-compatible extension values.
152pub type OAuthAuthorizationQuery = indexmap::IndexMap<String, alibi_core::utils::json::JsValue>;
153/// Application-owned asynchronous authorization request lookup.
154///
155/// Resolved parameters replace the URL query, except that the original client
156/// identity always wins. `None` means invalid or expired; errors propagate at
157/// the endpoint boundary. Values retain JavaScript numbers, including nonfinite values.
158#[async_trait]
159pub trait OAuthRequestUriResolver: Send + Sync + std::fmt::Debug {
160    async fn resolve(
161        &self,
162        input: OAuthRequestUriInput<'_>,
163    ) -> AuthResult<Option<OAuthAuthorizationQuery>>;
164}
165/// Immutable policy shared by authorization-server metadata and grant admission.
166#[derive(Clone, Debug)]
167pub struct OAuthProviderConfig {
168    /// Protocol quotas registered with the global rate limiter, not private API calls.
169    pub rate_limit: OAuthProviderRateLimits,
170    pub scopes: Vec<String>,
171    pub extensions: Vec<std::sync::Arc<OAuthProviderExtension>>,
172    /// Operator-owned `UserInfo` claims, applied after additive extension claims.
173    /// May override ordinary profile claims, but never the authoritative subject.
174    pub custom_user_info_claims: Option<std::sync::Arc<dyn OAuthUserInfoClaimsContributor>>,
175    /// Operator-owned ID-token claims. Profile claims may override additive
176    /// contributions; provider-owned protocol claims remain authoritative.
177    pub custom_id_token_claims: Option<std::sync::Arc<dyn OAuthTokenClaimsContributor>>,
178    /// Operator access claims, above per-issuance claims but below resource
179    /// policy. Opaque validation re-derives them without an issuance grant.
180    pub custom_access_token_claims: Option<std::sync::Arc<dyn OAuthTokenClaimsContributor>>,
181    /// Application response-envelope fields, collected before token writes.
182    /// Per-issuance fields take precedence; protocol response fields remain provider-owned.
183    pub custom_token_response_fields: Option<std::sync::Arc<dyn OAuthTokenClaimsContributor>>,
184    pub client_discoveries: Vec<std::sync::Arc<dyn OAuthClientDiscovery>>,
185    /// Managed identities cached by provider instance and protected from API mutations.
186    pub cached_trusted_clients: std::collections::HashSet<String>,
187    /// Global client-management policy, checked before client lookup or mutation.
188    /// Metadata and secret operations remain owner-controlled. Administrative
189    /// scopes-only patches additionally require the configure-scopes decision.
190    pub client_privileges: Option<std::sync::Arc<dyn OAuthClientPrivileges>>,
191    /// Operator decision for assigning client-credentials scopes.
192    /// This narrower policy does not authorize other management actions.
193    /// When both policies are configured, both must allow scope assignment.
194    pub client_credentials_scope_privileges: Option<std::sync::Arc<dyn OAuthClientPrivileges>>,
195    pub client_id_generator: Option<std::sync::Arc<dyn OAuthClientIdGenerator>>,
196    pub client_secret_generator: Option<std::sync::Arc<dyn OAuthClientSecretGenerator>>,
197    /// Override registered redirect matching after provider-owned URI safety checks.
198    pub validate_redirect_uri: Option<std::sync::Arc<dyn OAuthRedirectUriValidator>>,
199    /// Optional request registry; absent by default (`request_uri_not_supported`).
200    pub request_uri_resolver: Option<std::sync::Arc<dyn OAuthRequestUriResolver>>,
201    pub client_secret_hasher: Option<std::sync::Arc<dyn OAuthClientSecretHasher>>,
202    /// Application token hashing for authorization codes, access and refresh tokens.
203    pub token_hasher: Option<std::sync::Arc<dyn OAuthTokenHasher>>,
204    pub opaque_access_token_generator: Option<std::sync::Arc<dyn OAuthTokenGenerator>>,
205    pub refresh_token_generator: Option<std::sync::Arc<dyn OAuthTokenGenerator>>,
206    /// Presentation prefix for opaque access credentials; storage hashes plaintext.
207    pub opaque_access_token_prefix: Option<String>,
208    /// Presentation prefix for refresh credentials; storage hashes plaintext.
209    pub refresh_token_prefix: Option<String>,
210    /// Application-owned refresh presentation, separate from generation and hashing.
211    pub refresh_token_formatter: Option<std::sync::Arc<dyn OAuthRefreshTokenFormatter>>,
212    pub client_secret_cipher: Option<std::sync::Arc<dyn OAuthClientSecretCipher>>,
213    /// Wire prefix matching Better Auth's `prefix.clientSecret`; stored credentials exclude it.
214    pub client_secret_prefix: Option<String>,
215    pub login_page: String,
216    pub consent_page: String,
217    pub select_account_page: Option<String>,
218    pub select_account: Option<OAuthSelectAccountConfig>,
219    pub signup: Option<OAuthSignupConfig>,
220    pub post_login: Option<OAuthPostLoginConfig>,
221    pub code_expires_in: f64,
222    /// User access-token lifetime in seconds, including refresh grant issuance.
223    pub access_token_expires_in: f64,
224    /// Access-token lifetime for issuance without a user.
225    pub m2m_access_token_expires_in: f64,
226    pub id_token_expires_in: f64,
227    pub refresh_token_expires_in: f64,
228    /// Per-scope access-expiration ceilings. Numeric values are absolute Unix
229    /// seconds (and numeric zero is ignored), matching the reference policy.
230    pub scope_expirations: std::collections::HashMap<String, crate::jwt::JwtExpiration>,
231    /// Maximum assertion future expiration and issued-at age, in seconds (default 300).
232    pub assertion_max_lifetime: f64,
233    pub refresh_token_reuse_interval: f64,
234    pub allow_public_client_prelogin: bool,
235    pub enforce_per_client_resources: bool,
236    /// Opt-in resource-policy cache membership. Entries are process-global by
237    /// identifier, including across initialized stores, matching the pinned provider.
238    /// External writes remain live by default; successful admin CRUD evicts entries.
239    pub cached_resources: std::collections::HashSet<String>,
240    /// Configured resource identifiers or policy objects, using the provider's camelCase fields.
241    pub resources: Vec<Value>,
242    pub resource_seed_mode: OAuthResourceSeedMode,
243    pub identifier_validator: Option<std::sync::Arc<dyn OAuthResourceIdentifierValidator>>,
244    /// Optional operator decision for resource CRUD and link/unlink actions.
245    /// Without a hook, any authenticated session with headers may manage resources.
246    pub resource_privileges: Option<std::sync::Arc<dyn OAuthResourcePrivileges>>,
247    pub advertised_scopes: Option<Vec<String>>,
248    pub advertised_claims: Option<Vec<String>>,
249    pub grant_types: Vec<String>,
250    pub allow_dynamic_client_registration: bool,
251    /// Dynamic confidential clients may omit PKCE only when explicitly false.
252    /// Public and managed clients retain their existing PKCE policy.
253    pub client_registration_require_pkce: Option<bool>,
254    /// Baseline dynamic-registration capabilities. `None` uses provider scopes;
255    /// `Some(vec![])` deliberately supplies no baseline capabilities.
256    pub client_registration_default_scopes: Option<Vec<String>>,
257    /// Additional dynamic-registration capabilities, unioned after the baseline.
258    /// Requested registration scopes validate against, but do not narrow, this union.
259    pub client_registration_allowed_scopes: Option<Vec<String>>,
260    /// Server-default resource links for newly registered clients. Omitted and empty
261    /// lists both supply no links; identifiers must be present in `resources`.
262    pub client_registration_default_resources: Option<Vec<String>>,
263    /// Additional resource identifiers that dynamic registration may explicitly request.
264    /// Defaults remain admitted even when this list is empty.
265    pub client_registration_allowed_resources: Option<Vec<String>>,
266    /// Operator authorization for Bearer credentials presented without a genuine user session.
267    pub initial_access_token_validator:
268        Option<std::sync::Arc<dyn OAuthInitialAccessTokenValidator>>,
269    pub allow_unauthenticated_client_registration: bool,
270    pub disable_jwt_plugin: bool,
271    pub pairwise_secret: Option<String>,
272    pub dpop_signing_algorithms: Option<Vec<String>>,
273    pub dpop_proof_max_age_seconds: Option<f64>,
274}
275impl Default for OAuthProviderConfig {
276    fn default() -> Self {
277        Self {
278            rate_limit: OAuthProviderRateLimits::default(),
279            extensions: Vec::new(),
280            custom_user_info_claims: None,
281            custom_id_token_claims: None,
282            custom_access_token_claims: None,
283            custom_token_response_fields: None,
284            client_discoveries: Vec::new(),
285            cached_trusted_clients: std::collections::HashSet::new(),
286            client_privileges: None,
287            client_credentials_scope_privileges: None,
288            client_id_generator: None,
289            client_secret_generator: None,
290            validate_redirect_uri: None,
291            request_uri_resolver: None,
292            client_secret_hasher: None,
293            client_secret_cipher: None,
294            token_hasher: None,
295            opaque_access_token_generator: None,
296            refresh_token_generator: None,
297            opaque_access_token_prefix: None,
298            refresh_token_prefix: None,
299            refresh_token_formatter: None,
300            client_secret_prefix: None,
301            login_page: "/login".into(),
302            consent_page: "/consent".into(),
303            select_account_page: None,
304            select_account: None,
305            signup: None,
306            post_login: None,
307            code_expires_in: 600.0,
308            access_token_expires_in: 3600.0,
309            m2m_access_token_expires_in: 3600.0,
310            id_token_expires_in: 36_000.0,
311            refresh_token_expires_in: 2_592_000.0,
312            scope_expirations: std::collections::HashMap::new(),
313            assertion_max_lifetime: 300.0,
314            refresh_token_reuse_interval: 0.0,
315            allow_public_client_prelogin: false,
316            enforce_per_client_resources: true,
317            cached_resources: std::collections::HashSet::new(),
318            resources: Vec::new(),
319            resource_seed_mode: OAuthResourceSeedMode::default(),
320            identifier_validator: None,
321            resource_privileges: None,
322            scopes: ["openid", "profile", "email", "offline_access"]
323                .map(str::to_owned)
324                .into(),
325            advertised_scopes: None,
326            advertised_claims: None,
327            grant_types: ["authorization_code", "client_credentials", "refresh_token"]
328                .map(str::to_owned)
329                .into(),
330            allow_dynamic_client_registration: false,
331            client_registration_require_pkce: None,
332            client_registration_default_scopes: None,
333            client_registration_allowed_scopes: None,
334            client_registration_default_resources: None,
335            client_registration_allowed_resources: None,
336            initial_access_token_validator: None,
337            allow_unauthenticated_client_registration: false,
338            disable_jwt_plugin: false,
339            pairwise_secret: None,
340            dpop_signing_algorithms: None,
341            dpop_proof_max_age_seconds: None,
342        }
343    }
344}
345#[derive(Clone, Debug, Default)]
346pub struct OAuthProviderPlugin {
347    config: OAuthProviderConfig,
348    client_jwks: std::sync::Arc<client_jwks::Cache>,
349    trusted_clients: std::sync::Arc<discovery::TrustedCache>,
350}
351impl OAuthProviderPlugin {
352    #[must_use]
353    pub fn new() -> Self {
354        Self::default()
355    }
356    #[must_use]
357    pub fn with_config(mut config: OAuthProviderConfig) -> Self {
358        config.scopes = unique(
359            config
360                .scopes
361                .into_iter()
362                .filter(|scope| !scope.is_empty())
363                .collect(),
364        );
365        Self {
366            config,
367            ..Self::default()
368        }
369    }
370    /// Return the issuer's authorization-server configuration for trusted server callers.
371    #[must_use]
372    pub fn get_oauth_server_config_endpoint() -> ServerEndpoint<Value> {
373        ServerEndpoint::new("oauth-provider", "getOAuthServerConfig")
374    }
375    /// Return `OpenID` configuration when the provider supports the openid scope.
376    #[must_use]
377    pub fn get_open_id_config_endpoint() -> ServerEndpoint<Value> {
378        ServerEndpoint::new("oauth-provider", "getOpenIdConfig")
379    }
380    fn metadata<S: AuthSchema>(&self, ctx: &AuthContext<S>) -> AuthResult<Value> {
381        let jwt = if self.config.disable_jwt_plugin {
382            None
383        } else {
384            Some(
385                ctx.extensions
386                    .get::<super::jwt::JwtDiscoveryOptions>()
387                    .ok_or_else(|| AuthError::config("jwt_config"))?,
388            )
389        };
390        let base = format!(
391            "{}{}",
392            ctx.config.base_url.trim_end_matches('/'),
393            ctx.config.base_path
394        );
395        let issuer = jwt
396            .as_ref()
397            .and_then(|jwt| jwt.issuer.as_deref())
398            .unwrap_or(&base);
399        let issuer = normalize_issuer(issuer);
400        let grants = self.supported_grant_types();
401        let endpoint_auth = self.supported_auth_methods(false);
402        let mut token_auth = endpoint_auth.clone();
403        if self.config.allow_unauthenticated_client_registration
404            || !self.config.client_discoveries.is_empty()
405        {
406            token_auth.insert(0, "none".into());
407        }
408        let signing = [
409            "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512",
410            "EdDSA",
411        ];
412        let mut output: serde_json::Map<String, Value> = serde_json::from_value(json!({
413            "scopes_supported":self.config.advertised_scopes.as_ref().unwrap_or(&self.config.scopes),
414            "issuer":issuer,"authorization_endpoint":format!("{base}/oauth2/authorize"),"token_endpoint":format!("{base}/oauth2/token"),
415            "introspection_endpoint":format!("{base}/oauth2/introspect"),"revocation_endpoint":format!("{base}/oauth2/revoke"),
416            "response_types_supported":if grants.iter().any(|grant|grant=="authorization_code"){vec!["code"]}else{vec![]},
417            "response_modes_supported":["query"],"grant_types_supported":grants,
418            "token_endpoint_auth_methods_supported":token_auth,"token_endpoint_auth_signing_alg_values_supported":signing,
419            "introspection_endpoint_auth_methods_supported":endpoint_auth,
420            "introspection_endpoint_auth_signing_alg_values_supported":signing,
421            "revocation_endpoint_auth_methods_supported":endpoint_auth,
422            "revocation_endpoint_auth_signing_alg_values_supported":signing,
423            "code_challenge_methods_supported":["S256"],"authorization_response_iss_parameter_supported":true,
424            "dpop_signing_alg_values_supported":self.config.dpop_signing_algorithms.clone().unwrap_or_else(||["EdDSA","ES256","ES512","PS256","RS256"].map(str::to_owned).into()),
425            "backchannel_logout_supported":!self.config.disable_jwt_plugin,"backchannel_logout_session_supported":!self.config.disable_jwt_plugin,
426        }))?;
427        if let Some(jwt) = jwt.as_ref() {
428            _ = output.insert(
429                "jwks_uri".into(),
430                json!(
431                    jwt.remote_url
432                        .clone()
433                        .unwrap_or_else(|| format!("{base}{}", jwt.jwks_path))
434                ),
435            );
436        }
437        if self.config.allow_dynamic_client_registration {
438            _ = output.insert(
439                "registration_endpoint".into(),
440                json!(format!("{base}/oauth2/register")),
441            );
442        }
443        if self.config.scopes.iter().any(|scope| scope == "openid") {
444            _ = output.insert("claims_supported".into(), json!(self.supported_claims()));
445            _ = output.insert("claims_parameter_supported".into(), json!(true));
446            _ = output.insert(
447                "userinfo_endpoint".into(),
448                json!(format!("{base}/oauth2/userinfo")),
449            );
450            _ = output.insert(
451                "subject_types_supported".into(),
452                json!(if self
453                    .config
454                    .pairwise_secret
455                    .as_ref()
456                    .is_some_and(|secret| !secret.is_empty())
457                {
458                    vec!["public", "pairwise"]
459                } else {
460                    vec!["public"]
461                }),
462            );
463            _ = output.insert("acr_values_supported".into(), json!(["0"]));
464            _ = output.insert(
465                "id_token_signing_alg_values_supported".into(),
466                json!(jwt.as_ref().map_or_else(
467                    || vec!["HS256".to_owned()],
468                    |jwt| jwt.signing_algorithms.clone()
469                )),
470            );
471            _ = output.insert(
472                "end_session_endpoint".into(),
473                json!(format!("{base}/oauth2/end-session")),
474            );
475            _ = output.insert("request_parameter_supported".into(), json!(false));
476            _ = output.insert("request_uri_parameter_supported".into(), json!(false));
477            _ = output.insert(
478                "prompt_values_supported".into(),
479                json!(["login", "consent", "create", "select_account", "none"]),
480            );
481        }
482        let mut contributed = serde_json::Map::new();
483        for discovery in &self.config.client_discoveries {
484            contributed.extend(discovery.discovery_metadata());
485        }
486        contributed.extend(output);
487        self.apply_metadata_extensions(ctx, Value::Object(contributed))
488    }
489}
490fn unique(values: Vec<String>) -> Vec<String> {
491    let mut result = Vec::new();
492    for value in values {
493        if !result.contains(&value) {
494            result.push(value);
495        }
496    }
497    result
498}
499fn normalize_issuer(value: &str) -> String {
500    let Ok(mut url) = url::Url::parse(value) else {
501        return value.into();
502    };
503    let loopback = url.host_str().is_some_and(|host| {
504        host == "localhost" || host == "127.0.0.1" || host == "[::1]" || host == "::1"
505    });
506    if url.scheme() != "https" && !loopback {
507        _ = url.set_scheme("https");
508    }
509    url.set_query(None);
510    url.set_fragment(None);
511    url.to_string().trim_end_matches('/').to_owned()
512}
513#[async_trait]
514impl<S: AuthSchema> AuthPlugin<S> for OAuthProviderPlugin {
515    fn name(&self) -> &'static str {
516        "oauth-provider"
517    }
518    fn rate_limits(&self) -> Vec<alibi_core::PluginRateLimit> {
519        self.config.rate_limit.rules()
520    }
521    fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> {
522        vec![self]
523    }
524    async fn before_request(
525        &self,
526        req: &AuthRequest,
527        ctx: &AuthContext<S>,
528    ) -> AuthResult<Option<alibi_core::BeforeRequestAction>> {
529        let body = req.body_as_json::<alibi_core::utils::json::JsValue>().ok();
530        interaction::admit(
531            body.as_ref().and_then(|body| body.get("oauth_query")),
532            ctx.config.current_secret(),
533            req.extensions(),
534        )?
535        .map(|response| {
536            response
537                .to_response()
538                .map(alibi_core::BeforeRequestAction::Respond)
539        })
540        .transpose()
541    }
542    async fn after_request(
543        &self,
544        req: &AuthRequest,
545        ctx: &AuthContext<S>,
546        response: AuthResponse,
547    ) -> AuthResult<AuthResponse> {
548        let Some(mut resumed) = interaction::resume(self, req, &response.headers, ctx).await?
549        else {
550            return Ok(response);
551        };
552        resumed.merge_headers(response.headers);
553        resumed.to_response()
554    }
555    fn allowed_media_types(&self, route: &AuthRoute) -> Vec<&'static str> {
556        match route.path.as_str() {
557            "/oauth2/authorize"
558            | "/oauth2/end-session/confirm"
559            | "/oauth2/token"
560            | "/oauth2/introspect"
561            | "/oauth2/revoke"
562            | "/oauth2/userinfo" => vec!["application/x-www-form-urlencoded"],
563            "/oauth2/end-session" => vec!["application/json", "application/x-www-form-urlencoded"],
564            _ => vec!["application/json"],
565        }
566    }
567    fn routes(&self) -> Vec<AuthRoute> {
568        vec![
569            AuthRoute::post(
570                "/oauth2/end-session/confirm",
571                "oauth2EndSessionConfirmation",
572            ),
573            AuthRoute::get("/oauth2/end-session", "oauth2EndSession"),
574            AuthRoute::post("/oauth2/end-session", "oauth2EndSession"),
575            AuthRoute::get("/oauth2/userinfo", "oauth2UserInfo"),
576            AuthRoute::post("/oauth2/userinfo", "oauth2UserInfo"),
577            AuthRoute::post("/oauth2/revoke", "oauth2Revoke"),
578            AuthRoute::post("/oauth2/introspect", "oauth2Introspect"),
579            AuthRoute::post("/oauth2/token", "oauth2Token"),
580            AuthRoute::post("/oauth2/create-client", "createOAuthClient"),
581            AuthRoute::get("/oauth2/get-client", "getOAuthClient"),
582            AuthRoute::get("/oauth2/public-client", "getOAuthClientPublic"),
583            AuthRoute::post(
584                "/oauth2/public-client-prelogin",
585                "getOAuthClientPublicPrelogin",
586            ),
587            AuthRoute::get("/oauth2/get-clients", "getOAuthClients"),
588            AuthRoute::post("/oauth2/delete-client", "deleteOAuthClient"),
589            AuthRoute::post("/oauth2/update-client", "updateOAuthClient"),
590            AuthRoute::post("/oauth2/register", "registerOAuthClient"),
591            AuthRoute::post("/oauth2/consent", "oauth2Consent"),
592            AuthRoute::post("/oauth2/continue", "oauth2Continue"),
593            AuthRoute::get("/oauth2/get-consents", "getOAuthConsents"),
594            AuthRoute::get("/oauth2/get-consent", "getOAuthConsent"),
595            AuthRoute::post("/oauth2/update-consent", "updateOAuthConsent"),
596            AuthRoute::post("/oauth2/delete-consent", "deleteOAuthConsent"),
597            AuthRoute::get("/oauth2/authorize", "oauth2Authorize"),
598            AuthRoute::post("/oauth2/authorize", "oauth2Authorize"),
599            AuthRoute::post("/oauth2/client/rotate-secret", "rotateClientSecret"),
600        ]
601    }
602    fn server_endpoints(&self) -> Vec<EndpointDefinition> {
603        vec![
604            crate::endpoint::definition(
605                "oauth2EndSessionConfirmation",
606                "oauth2EndSessionConfirmation",
607                Some("/oauth2/end-session/confirm"),
608                HttpMethod::Post,
609            ),
610            crate::endpoint::definition(
611                "oauth2EndSession",
612                "oauth2EndSession",
613                Some("/oauth2/end-session"),
614                HttpMethod::Get,
615            ),
616            crate::endpoint::definition(
617                "oauth2UserInfo",
618                "oauth2UserInfo",
619                Some("/oauth2/userinfo"),
620                HttpMethod::Get,
621            ),
622            crate::endpoint::definition(
623                "oauth2Revoke",
624                "oauth2Revoke",
625                Some("/oauth2/revoke"),
626                HttpMethod::Post,
627            ),
628            crate::endpoint::definition(
629                "oauth2Introspect",
630                "oauth2Introspect",
631                Some("/oauth2/introspect"),
632                HttpMethod::Post,
633            ),
634            crate::endpoint::definition(
635                "oauth2Token",
636                "oauth2Token",
637                Some("/oauth2/token"),
638                HttpMethod::Post,
639            ),
640            crate::endpoint::definition(
641                "oauth2Continue",
642                "oauth2Continue",
643                Some("/oauth2/continue"),
644                HttpMethod::Post,
645            ),
646            crate::endpoint::definition(
647                "adminUnlinkClientResource",
648                "adminUnlinkClientResource",
649                Some("/admin/oauth2/resources/:identifier/clients/:client_id"),
650                HttpMethod::Delete,
651            ),
652            crate::endpoint::definition(
653                "adminLinkClientResource",
654                "adminLinkClientResource",
655                Some("/admin/oauth2/resources/:identifier/clients/:client_id"),
656                HttpMethod::Post,
657            ),
658            crate::endpoint::definition(
659                "adminDeleteOAuthResource",
660                "adminDeleteOAuthResource",
661                Some("/admin/oauth2/resources/:identifier"),
662                HttpMethod::Delete,
663            ),
664            crate::endpoint::definition(
665                "adminUpdateOAuthResource",
666                "adminUpdateOAuthResource",
667                Some("/admin/oauth2/resources/:identifier"),
668                HttpMethod::Patch,
669            ),
670            crate::endpoint::definition(
671                "adminGetOAuthResource",
672                "adminGetOAuthResource",
673                Some("/admin/oauth2/resources/:identifier"),
674                HttpMethod::Get,
675            ),
676            crate::endpoint::definition(
677                "adminListOAuthResources",
678                "adminListOAuthResources",
679                Some("/admin/oauth2/resources"),
680                HttpMethod::Get,
681            ),
682            crate::endpoint::definition(
683                "adminCreateOAuthResource",
684                "adminCreateOAuthResource",
685                Some("/admin/oauth2/resources"),
686                HttpMethod::Post,
687            ),
688            crate::endpoint::definition(
689                "deleteOAuthConsent",
690                "deleteOAuthConsent",
691                Some("/oauth2/delete-consent"),
692                HttpMethod::Post,
693            ),
694            crate::endpoint::definition(
695                "updateOAuthConsent",
696                "updateOAuthConsent",
697                Some("/oauth2/update-consent"),
698                HttpMethod::Post,
699            ),
700            crate::endpoint::definition(
701                "getOAuthConsent",
702                "getOAuthConsent",
703                Some("/oauth2/get-consent"),
704                HttpMethod::Get,
705            ),
706            crate::endpoint::definition(
707                "getOAuthConsents",
708                "getOAuthConsents",
709                Some("/oauth2/get-consents"),
710                HttpMethod::Get,
711            ),
712            crate::endpoint::definition(
713                "oauth2Consent",
714                "oauth2Consent",
715                Some("/oauth2/consent"),
716                HttpMethod::Post,
717            ),
718            crate::endpoint::definition(
719                "getOAuthClientPublicPrelogin",
720                "getOAuthClientPublicPrelogin",
721                Some("/oauth2/public-client-prelogin"),
722                HttpMethod::Post,
723            ),
724            crate::endpoint::definition(
725                "oauth2Authorize",
726                "oauth2Authorize",
727                Some("/oauth2/authorize"),
728                HttpMethod::Get,
729            ),
730            crate::endpoint::definition(
731                "adminUpdateOAuthClient",
732                "adminUpdateOAuthClient",
733                Some("/admin/oauth2/update-client"),
734                HttpMethod::Patch,
735            ),
736            crate::endpoint::definition(
737                "adminCreateOAuthClient",
738                "adminCreateOAuthClient",
739                Some("/admin/oauth2/create-client"),
740                HttpMethod::Post,
741            ),
742            crate::endpoint::definition(
743                "registerOAuthClient",
744                "registerOAuthClient",
745                Some("/oauth2/register"),
746                HttpMethod::Post,
747            ),
748            crate::endpoint::definition(
749                "updateOAuthClient",
750                "updateOAuthClient",
751                Some("/oauth2/update-client"),
752                HttpMethod::Post,
753            ),
754            crate::endpoint::definition(
755                "rotateClientSecret",
756                "rotateClientSecret",
757                Some("/oauth2/client/rotate-secret"),
758                HttpMethod::Post,
759            ),
760            crate::endpoint::definition(
761                "deleteOAuthClient",
762                "deleteOAuthClient",
763                Some("/oauth2/delete-client"),
764                HttpMethod::Post,
765            ),
766            crate::endpoint::definition(
767                "getOAuthClients",
768                "getOAuthClients",
769                Some("/oauth2/get-clients"),
770                HttpMethod::Get,
771            ),
772            crate::endpoint::definition(
773                "getOAuthClientPublic",
774                "getOAuthClientPublic",
775                Some("/oauth2/public-client"),
776                HttpMethod::Get,
777            ),
778            crate::endpoint::definition(
779                "getOAuthClient",
780                "getOAuthClient",
781                Some("/oauth2/get-client"),
782                HttpMethod::Get,
783            ),
784            crate::endpoint::definition(
785                "createOAuthClient",
786                "createOAuthClient",
787                Some("/oauth2/create-client"),
788                HttpMethod::Post,
789            ),
790            crate::endpoint::definition(
791                "getOAuthServerConfig",
792                "getOAuthServerConfig",
793                Some("/.well-known/oauth-authorization-server"),
794                HttpMethod::Get,
795            ),
796            crate::endpoint::definition(
797                "getOpenIdConfig",
798                "getOpenIdConfig",
799                Some("/.well-known/openid-configuration"),
800                HttpMethod::Get,
801            ),
802        ]
803    }
804    fn validate_endpoint(
805        &self,
806        call: &EndpointCall,
807        _ctx: &AuthContext<S>,
808    ) -> AuthResult<EndpointInput> {
809        Ok(EndpointInput {
810            body: if call.operation_id() == "oauth2EndSessionConfirmation" {
811                Some(crate::endpoint::validate_fields(
812                    call.body(),
813                    "body",
814                    &[crate::authentication_helpers::JsonField {
815                        name: "action",
816                        kind: crate::authentication_helpers::JsonFieldKind::OneOf(&["confirm"]),
817                        required: true,
818                    }],
819                )?)
820            } else if call.operation_id() == "oauth2EndSession" {
821                logout::validate(call.body(), "body")?
822            } else if call.operation_id() == "oauth2UserInfo" {
823                call.body()
824                    .map(|body| {
825                        crate::endpoint::validate_fields(
826                            Some(body),
827                            "body",
828                            &[crate::authentication_helpers::JsonField::string(
829                                "access_token",
830                                false,
831                            )],
832                        )
833                    })
834                    .transpose()?
835            } else if call.operation_id() == "oauth2Continue" {
836                Some(continuation::validate(call.body())?)
837            } else if call.operation_id() == "adminUpdateOAuthResource" {
838                Some(resources::validate_update(call.body())?)
839            } else if call.operation_id() == "adminCreateOAuthResource" {
840                Some(resources::validate_create(call.body())?)
841            } else if call.operation_id() == "deleteOAuthConsent" {
842                Some(consent::validate_id(call.body(), "body")?)
843            } else if call.operation_id() == "updateOAuthConsent" {
844                Some(consent::validate_update(call.body())?)
845            } else if call.operation_id() == "oauth2Consent" {
846                Some(consent::validate(call.body())?)
847            } else if call.operation_id() == "getOAuthClientPublicPrelogin" {
848                Some(clients::validate_prelogin(call.body())?)
849            } else if call.operation_id() == "adminUpdateOAuthClient" {
850                Some(clients::validate_admin_update(call.body())?)
851            } else if call.operation_id() == "adminCreateOAuthClient" {
852                Some(clients::validate_admin_create(call.body())?)
853            } else if call.operation_id() == "updateOAuthClient" {
854                Some(clients::validate_update(call.body())?)
855            } else if matches!(
856                call.operation_id(),
857                "deleteOAuthClient" | "rotateClientSecret"
858            ) {
859                Some(clients::validate_delete(call.body())?)
860            } else if call.operation_id() == "createOAuthClient" {
861                Some(clients::validate(call.body())?)
862            } else {
863                call.body().cloned()
864            },
865            query: if call.operation_id() == "oauth2EndSession" {
866                logout::validate(call.query(), "query")?
867            } else if call.operation_id() == "getOAuthConsent" {
868                Some(consent::validate_id(call.query(), "query")?)
869            } else if matches!(
870                call.operation_id(),
871                "getOAuthClient" | "getOAuthClientPublic"
872            ) {
873                Some(clients::validate_read(call.query())?)
874            } else {
875                call.query().cloned()
876            },
877        })
878    }
879    async fn on_endpoint(
880        &self,
881        call: &EndpointCall,
882        ctx: &AuthContext<S>,
883    ) -> AuthResult<EndpointResponse> {
884        if call.operation_id() == "oauth2EndSessionConfirmation" {
885            return Box::pin(self.confirm_logout(call, ctx)).await;
886        }
887        if call.operation_id() == "oauth2EndSession" {
888            return Box::pin(self.end_session(call.query(), call.body(), call, ctx)).await;
889        }
890        if call.operation_id() == "oauth2UserInfo" {
891            return Box::pin(self.user_info(call.body(), call, ctx)).await;
892        }
893        if call.operation_id() == "oauth2Revoke" {
894            return Box::pin(self.revoke(call.body(), call, ctx)).await;
895        }
896        if call.operation_id() == "oauth2Introspect" {
897            return Box::pin(self.introspect(call.body(), call, ctx)).await;
898        }
899        if call.operation_id() == "oauth2Token" {
900            return Box::pin(self.token(call.body(), call, ctx)).await;
901        }
902        if call.operation_id() == "oauth2Continue" {
903            return Box::pin(self.continue_authorization(call.body_as()?, call, ctx)).await;
904        }
905        if call.operation_id() == "adminUnlinkClientResource" {
906            return Box::pin(self.unlink_resource(call.params(), call, ctx)).await;
907        }
908        if call.operation_id() == "adminLinkClientResource" {
909            return Box::pin(self.link_resource(call.params(), call, ctx)).await;
910        }
911        if call.operation_id() == "adminDeleteOAuthResource" {
912            return Box::pin(self.delete_resource(call.params(), call, ctx)).await;
913        }
914        if call.operation_id() == "adminUpdateOAuthResource" {
915            return Box::pin(self.update_resource(call.params(), call.body_as()?, call, ctx)).await;
916        }
917        if call.operation_id() == "adminGetOAuthResource" {
918            return Box::pin(self.read_resource(call.params(), call, ctx)).await;
919        }
920        if call.operation_id() == "adminListOAuthResources" {
921            return Box::pin(self.list_resources(call, ctx)).await;
922        }
923        if call.operation_id() == "adminCreateOAuthResource" {
924            return Box::pin(self.create_resource(call.body_as()?, call, ctx)).await;
925        }
926        if call.operation_id() == "deleteOAuthConsent" {
927            return Box::pin(self.delete_consent(call.body_as()?, call, ctx)).await;
928        }
929        if call.operation_id() == "updateOAuthConsent" {
930            return Box::pin(self.update_consent(call.body_as()?, call, ctx)).await;
931        }
932        if call.operation_id() == "getOAuthConsent" {
933            return Box::pin(self.read_consent(call.query_as()?, call, ctx)).await;
934        }
935        if call.operation_id() == "getOAuthConsents" {
936            return Box::pin(self.list_consents(call, ctx)).await;
937        }
938        if call.operation_id() == "oauth2Consent" {
939            return Box::pin(self.consent(call.body_as()?, call, ctx)).await;
940        }
941        if call.operation_id() == "getOAuthClientPublicPrelogin" {
942            return Box::pin(self.read_prelogin_client(call.body_as()?, ctx)).await;
943        }
944        if call.operation_id() == "oauth2Authorize" {
945            let value = if call.method() == Some(&HttpMethod::Post) {
946                call.body()
947            } else {
948                call.query()
949            };
950            return Box::pin(self.authorize(value.cloned(), call, ctx)).await;
951        }
952        if call.operation_id() == "adminUpdateOAuthClient" {
953            return Box::pin(self.update_client(call.body_as()?, call, ctx, true)).await;
954        }
955        if call.operation_id() == "adminCreateOAuthClient" {
956            return Box::pin(self.admin_create_client(call.body_as()?, call, ctx)).await;
957        }
958        if call.operation_id() == "registerOAuthClient" {
959            return Box::pin(self.register_client(call.body(), call, ctx)).await;
960        }
961        if call.operation_id() == "updateOAuthClient" {
962            return Box::pin(self.update_client(call.body_as()?, call, ctx, false)).await;
963        }
964        if call.operation_id() == "rotateClientSecret" {
965            return Box::pin(self.rotate_client_secret(call.body_as()?, call, ctx)).await;
966        }
967        if call.operation_id() == "deleteOAuthClient" {
968            return Box::pin(self.delete_client(call.body_as()?, call, ctx)).await;
969        }
970        if call.operation_id() == "getOAuthClients" {
971            return Box::pin(self.list_clients(call, ctx)).await;
972        }
973        if call.operation_id() == "getOAuthClientPublic" {
974            return Box::pin(self.read_public_client(call.query_as()?, call, ctx)).await;
975        }
976        if call.operation_id() == "getOAuthClient" {
977            return Box::pin(self.read_client(call.query_as()?, call, ctx)).await;
978        }
979        if call.operation_id() == "createOAuthClient" {
980            return Box::pin(self.create_client(call.body_as()?, call, ctx)).await;
981        }
982        if call.operation_id() == "getOpenIdConfig"
983            && !self.config.scopes.iter().any(|scope| scope == "openid")
984        {
985            return Ok(EndpointResponse::error(AuthError::Api {
986                status: 404,
987                code: None,
988                message: String::new(),
989            })
990            .without_error_body()
991            .with_header("Content-Type", "application/json"));
992        }
993        EndpointResponse::json(&self.metadata(ctx)?)
994    }
995    async fn on_request(
996        &self,
997        req: &AuthRequest,
998        ctx: &AuthContext<S>,
999    ) -> AuthResult<Option<AuthResponse>> {
1000        if req.path() == "/oauth2/end-session/confirm" && *req.method() == HttpMethod::Post {
1001            let action = url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
1002                .find(|(key, _)| key == "action")
1003                .map(|(_, value)| value.into_owned());
1004            if action.as_deref() != Some("confirm") {
1005                return Ok(Some(
1006                    AuthError::bad_request("Invalid logout action").to_auth_response(),
1007                ));
1008            }
1009            return Ok(Some(
1010                Box::pin(self.confirm_logout(req, ctx))
1011                    .await?
1012                    .to_response()?,
1013            ));
1014        }
1015        if req.path() == "/oauth2/end-session"
1016            && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
1017        {
1018            let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
1019            let body = if *req.method() == HttpMethod::Post {
1020                Some(alibi_core::utils::json::parse_value(
1021                    &serde_json::to_string(
1022                        &url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
1023                            .into_owned()
1024                            .collect::<std::collections::HashMap<_, _>>(),
1025                    )?,
1026                )?)
1027            } else {
1028                None
1029            };
1030            return Ok(Some(
1031                Box::pin(self.end_session(Some(&query), body.as_ref(), req, ctx))
1032                    .await?
1033                    .to_response()?,
1034            ));
1035        }
1036        if req.path() == "/oauth2/userinfo"
1037            && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
1038        {
1039            let body = if *req.method() == HttpMethod::Post {
1040                Some(req.body_as_json::<alibi_core::utils::json::JsValue>()?)
1041            } else {
1042                None
1043            };
1044            // The admitted body may now have JSON dispatch bytes. Keep it
1045            // separate from the genuine Request used for POST/DPoP binding.
1046            let call = alibi_core::endpoint::current_endpoint_call_context().unwrap_or_else(|| {
1047                EndpointCall::from_http_request(
1048                    req,
1049                    &AuthRoute::get("/oauth2/userinfo", "oauth2UserInfo"),
1050                )
1051            });
1052            return Ok(Some(
1053                Box::pin(self.user_info(body.as_ref(), &call, ctx))
1054                    .await?
1055                    .to_response()?,
1056            ));
1057        }
1058        if req.path() == "/oauth2/token" && *req.method() == HttpMethod::Post {
1059            // Dispatch has already decoded the admitted form body. Endpoint
1060            // context patches may rewrite its bytes as JSON, so reparsing those
1061            // bytes as a form loses the grant and every credential binding.
1062            let body = req.body_as_json::<alibi_core::utils::json::JsValue>()?;
1063            // Credential duplicate/method checks still need the original form
1064            // Request retained by the call, not the rewritten dispatch bytes.
1065            let call = alibi_core::endpoint::current_endpoint_call_context().unwrap_or_else(|| {
1066                EndpointCall::from_http_request(
1067                    req,
1068                    &AuthRoute::post("/oauth2/token", "oauth2Token"),
1069                )
1070            });
1071            let call = call.handler_context(Some(body.clone()), call.query().cloned());
1072            return Ok(Some(
1073                Box::pin(self.token(Some(&body), &call, ctx))
1074                    .await?
1075                    .to_response()?,
1076            ));
1077        }
1078        if matches!(req.path(), "/oauth2/introspect" | "/oauth2/revoke")
1079            && *req.method() == HttpMethod::Post
1080        {
1081            // Consume admitted logical input (including form values and hook
1082            // patches), but authenticate against the original physical Request.
1083            let body = req.body_as_json::<alibi_core::utils::json::JsValue>()?;
1084            let revoking = req.path() == "/oauth2/revoke";
1085            let call = alibi_core::endpoint::current_endpoint_call_context().unwrap_or_else(|| {
1086                EndpointCall::from_http_request(
1087                    req,
1088                    &AuthRoute::post(
1089                        req.path(),
1090                        if revoking {
1091                            "oauth2Revoke"
1092                        } else {
1093                            "oauth2Introspect"
1094                        },
1095                    ),
1096                )
1097            });
1098            let call = call.handler_context(Some(body.clone()), call.query().cloned());
1099            let response = if revoking {
1100                Box::pin(self.revoke(Some(&body), &call, ctx)).await?
1101            } else {
1102                Box::pin(self.introspect(Some(&body), &call, ctx)).await?
1103            };
1104            return Ok(Some(response.to_response()?));
1105        }
1106        if req.path() == "/oauth2/authorize"
1107            && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
1108        {
1109            let value = if *req.method() == HttpMethod::Post {
1110                req.body_as_json::<alibi_core::utils::json::JsValue>()?
1111            } else {
1112                req.query_as_js_value()
1113            };
1114            // Legacy HTTP dispatch need not carry a logical endpoint frame.
1115            // Build it from the admitted route and genuine transport request,
1116            // not by rewriting that Request to contain resolver parameters.
1117            let call = alibi_core::endpoint::current_endpoint_call_context().unwrap_or_else(|| {
1118                EndpointCall::from_http_request(
1119                    req,
1120                    &AuthRoute::get("/oauth2/authorize", "oauth2Authorize"),
1121                )
1122            });
1123            let call = call.handler_context(call.body().cloned(), Some(value.clone()));
1124            return Ok(Some(
1125                alibi_core::endpoint::with_endpoint_call_context(
1126                    call,
1127                    Box::pin(self.authorize(Some(value), req, ctx)),
1128                )
1129                .await?
1130                .to_response()?,
1131            ));
1132        }
1133        if req.path() == "/oauth2/register" && *req.method() == HttpMethod::Post {
1134            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
1135            return Ok(Some(
1136                Box::pin(self.register_client(Some(&body), req, ctx))
1137                    .await?
1138                    .to_response()?,
1139            ));
1140        }
1141        if req.path() == "/oauth2/update-client" && *req.method() == HttpMethod::Post {
1142            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
1143            let input =
1144                alibi_core::utils::json::from_value(clients::validate_update(Some(&body))?)?;
1145            return Ok(Some(
1146                Box::pin(self.update_client(input, req, ctx, false))
1147                    .await?
1148                    .to_response()?,
1149            ));
1150        }
1151        if matches!(
1152            req.path(),
1153            "/oauth2/delete-client" | "/oauth2/client/rotate-secret"
1154        ) && *req.method() == HttpMethod::Post
1155        {
1156            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
1157            let input =
1158                alibi_core::utils::json::from_value(clients::validate_delete(Some(&body))?)?;
1159            if req.path() == "/oauth2/client/rotate-secret" {
1160                return Ok(Some(
1161                    Box::pin(self.rotate_client_secret(input, req, ctx))
1162                        .await?
1163                        .to_response()?,
1164                ));
1165            }
1166            return Ok(Some(
1167                Box::pin(self.delete_client(input, req, ctx))
1168                    .await?
1169                    .to_response()?,
1170            ));
1171        }
1172        if req.path() == "/oauth2/public-client-prelogin" && *req.method() == HttpMethod::Post {
1173            let body = req.body_as_json()?;
1174            let input =
1175                alibi_core::utils::json::from_value(clients::validate_prelogin(Some(&body))?)?;
1176            return Ok(Some(
1177                Box::pin(self.read_prelogin_client(input, ctx))
1178                    .await?
1179                    .to_response()?,
1180            ));
1181        }
1182        if req.path() == "/oauth2/continue" && *req.method() == HttpMethod::Post {
1183            let body = req.body_as_json()?;
1184            let input = alibi_core::utils::json::from_value(continuation::validate(Some(&body))?)?;
1185            return Ok(Some(
1186                Box::pin(self.continue_authorization(input, req, ctx))
1187                    .await?
1188                    .to_response()?,
1189            ));
1190        }
1191        if req.path() == "/oauth2/consent" && *req.method() == HttpMethod::Post {
1192            let body = req.body_as_json()?;
1193            let input = alibi_core::utils::json::from_value(consent::validate(Some(&body))?)?;
1194            return Ok(Some(
1195                Box::pin(self.consent(input, req, ctx))
1196                    .await?
1197                    .to_response()?,
1198            ));
1199        }
1200        if req.path() == "/oauth2/delete-consent" && *req.method() == HttpMethod::Post {
1201            let body = req.body_as_json()?;
1202            let input =
1203                alibi_core::utils::json::from_value(consent::validate_id(Some(&body), "body")?)?;
1204            return Ok(Some(
1205                Box::pin(self.delete_consent(input, req, ctx))
1206                    .await?
1207                    .to_response()?,
1208            ));
1209        }
1210        if req.path() == "/oauth2/update-consent" && *req.method() == HttpMethod::Post {
1211            let body = req.body_as_json()?;
1212            let input =
1213                alibi_core::utils::json::from_value(consent::validate_update(Some(&body))?)?;
1214            return Ok(Some(
1215                Box::pin(self.update_consent(input, req, ctx))
1216                    .await?
1217                    .to_response()?,
1218            ));
1219        }
1220        if req.path() == "/oauth2/get-consent" && *req.method() == HttpMethod::Get {
1221            let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
1222            let input =
1223                alibi_core::utils::json::from_value(consent::validate_id(Some(&query), "query")?)?;
1224            return Ok(Some(
1225                Box::pin(self.read_consent(input, req, ctx))
1226                    .await?
1227                    .to_response()?,
1228            ));
1229        }
1230        if req.path() == "/oauth2/get-consents" && *req.method() == HttpMethod::Get {
1231            return Ok(Some(
1232                Box::pin(self.list_consents(req, ctx))
1233                    .await?
1234                    .to_response()?,
1235            ));
1236        }
1237        if req.path() == "/oauth2/get-clients" && *req.method() == HttpMethod::Get {
1238            return Ok(Some(
1239                Box::pin(self.list_clients(req, ctx)).await?.to_response()?,
1240            ));
1241        }
1242        if matches!(req.path(), "/oauth2/get-client" | "/oauth2/public-client")
1243            && *req.method() == HttpMethod::Get
1244        {
1245            let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
1246            let input = alibi_core::utils::json::from_value(clients::validate_read(Some(&query))?)?;
1247            if req.path() == "/oauth2/public-client" {
1248                return Ok(Some(
1249                    Box::pin(self.read_public_client(input, req, ctx))
1250                        .await?
1251                        .to_response()?,
1252                ));
1253            }
1254            return Ok(Some(
1255                Box::pin(self.read_client(input, req, ctx))
1256                    .await?
1257                    .to_response()?,
1258            ));
1259        }
1260        if req.path() == "/oauth2/create-client" && *req.method() == HttpMethod::Post {
1261            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
1262            let input = alibi_core::utils::json::from_value(clients::validate(Some(&body))?)?;
1263            return Ok(Some(
1264                Box::pin(self.create_client(input, req, ctx))
1265                    .await?
1266                    .to_response()?,
1267            ));
1268        }
1269        Ok(None)
1270    }
1271    async fn on_init(&self, ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
1272        self.validate_extensions()?;
1273        for scope in self
1274            .config
1275            .client_registration_allowed_scopes
1276            .iter()
1277            .flatten()
1278            .chain(
1279                self.config
1280                    .client_registration_default_scopes
1281                    .iter()
1282                    .flatten(),
1283            )
1284        {
1285            if !self.config.scopes.contains(scope) {
1286                return Err(AuthError::config(format!(
1287                    "clientRegistrationAllowedScope {scope} not found in scopes"
1288                )));
1289            }
1290        }
1291        if self
1292            .config
1293            .advertised_scopes
1294            .as_ref()
1295            .is_some_and(|scopes| {
1296                scopes
1297                    .iter()
1298                    .any(|scope| !self.config.scopes.contains(scope))
1299            })
1300        {
1301            return Err(AuthError::config(
1302                "advertisedMetadata.scopes_supported contains an unsupported scope",
1303            ));
1304        }
1305        let configured_resources = self
1306            .config
1307            .resources
1308            .iter()
1309            .filter_map(|resource| {
1310                resource
1311                    .as_str()
1312                    .or_else(|| resource.get("identifier").and_then(Value::as_str))
1313            })
1314            .collect::<std::collections::HashSet<_>>();
1315        for (option, identifiers) in [
1316            (
1317                "clientRegistrationDefaultResources",
1318                &self.config.client_registration_default_resources,
1319            ),
1320            (
1321                "clientRegistrationAllowedResources",
1322                &self.config.client_registration_allowed_resources,
1323            ),
1324        ] {
1325            for identifier in identifiers.iter().flatten() {
1326                if !configured_resources.contains(identifier.as_str()) {
1327                    return Err(AuthError::config(format!(
1328                        "{option} resource {identifier} not found in resources"
1329                    )));
1330                }
1331            }
1332        }
1333        if self
1334            .config
1335            .pairwise_secret
1336            .as_ref()
1337            .is_some_and(|secret| !secret.is_empty() && secret.len() < 32)
1338        {
1339            return Err(AuthError::config(
1340                "pairwiseSecret must be at least 32 characters long for adequate HMAC-SHA256 security",
1341            ));
1342        }
1343        if self
1344            .config
1345            .grant_types
1346            .iter()
1347            .any(|grant| grant == "refresh_token")
1348            && !self
1349                .config
1350                .grant_types
1351                .iter()
1352                .any(|grant| grant == "authorization_code")
1353        {
1354            return Err(AuthError::config(
1355                "refresh_token grant requires authorization_code grant",
1356            ));
1357        }
1358        if self.config.disable_jwt_plugin && self.config.client_secret_hasher.is_some() {
1359            return Err(AuthError::config(
1360                "unable to store hashed secrets because id tokens will be signed with secret",
1361            ));
1362        }
1363        if !self.config.disable_jwt_plugin && self.config.client_secret_cipher.is_some() {
1364            return Err(AuthError::config(
1365                "encryption method not recommended, please use 'hashed' or the 'hash' function",
1366            ));
1367        }
1368        let mut discovery_ids = std::collections::HashSet::new();
1369        for discovery in &self.config.client_discoveries {
1370            if alibi_core::utils::javascript::trim(discovery.id()).is_empty() {
1371                return Err(AuthError::config(
1372                    "OAuth Provider extension client discovery id cannot be empty",
1373                ));
1374            }
1375            if !discovery_ids.insert(discovery.id()) {
1376                return Err(AuthError::config(format!(
1377                    "OAuth Provider extensions register client discovery id \"{}\" more than once. Extension contributions must be disjoint.",
1378                    discovery.id()
1379                )));
1380            }
1381        }
1382        self.seed_resources(ctx.database.as_ref()).await?;
1383        ctx.extensions.insert(resource_seed::SeedState::default());
1384        ctx.extensions.insert(discovery::TrustedClients {
1385            ids: self.config.cached_trusted_clients.clone(),
1386            cache: self.trusted_clients.clone(),
1387        });
1388        ctx.extensions.insert(discovery::ClientDiscoveries(
1389            self.config.client_discoveries.clone(),
1390        ));
1391        ctx.register_session_deletion_hook(std::sync::Arc::new(
1392            session_retirement::TokenRetirementHook(self.clone()),
1393        ));
1394        Ok(())
1395    }
1396}