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