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 client_jwks;
9mod client_secret;
10mod clients;
11mod consent;
12mod continuation;
13mod credentials;
14mod discovery;
15mod dpop;
16mod id_token;
17mod issuance;
18pub use issuance::OAuthTokenIssueParams;
19mod introspection;
20mod logout;
21mod machine;
22mod refresh;
23mod resource_policy;
24mod resources;
25mod revocation;
26mod session_retirement;
27mod token;
28mod token_storage;
29pub use token_storage::OAuthTokenHasher;
30mod userinfo;
31use alibi_core::endpoint::{
32    EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
33};
34use alibi_core::{
35    AuthContext, AuthError, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
36    AuthSchema, HttpMethod,
37};
38pub use assertion::consume_client_assertion;
39use async_trait::async_trait;
40pub use client_jwks::{
41    OAuthClientMetadataAbortSignal, OAuthClientMetadataFetchOptions,
42    OAuthClientMetadataFetchResponse, OAuthClientMetadataResourceFetch,
43};
44pub use clients::CreateOAuthClientInput;
45pub use discovery::OAuthClientDiscovery;
46use serde_json::{Value, json};
47
48/// Actual authenticated context passed to an operator client-privilege decision.
49#[derive(Debug, Clone)]
50pub struct OAuthClientPrivilegeContext {
51    pub action: &'static str,
52    pub headers: alibi_core::Headers,
53    pub user: Value,
54    pub session: Value,
55}
56#[async_trait]
57pub trait OAuthClientPrivileges: Send + Sync + std::fmt::Debug {
58    async fn allowed(&self, context: &OAuthClientPrivilegeContext) -> AuthResult<bool>;
59}
60/// Application-owned client identifiers. Empty output is preserved, matching the reference.
61pub trait OAuthClientIdGenerator: Send + Sync + std::fmt::Debug {
62    fn generate(&self) -> AuthResult<String>;
63}
64/// Application-owned client credential generation. Empty output uses the default generator.
65pub trait OAuthClientSecretGenerator: Send + Sync + std::fmt::Debug {
66    fn generate(&self) -> AuthResult<String>;
67}
68/// Immutable policy shared by authorization-server metadata and grant admission.
69#[derive(Clone, Debug)]
70pub struct OAuthProviderConfig {
71    pub scopes: Vec<String>,
72    pub client_discoveries: Vec<std::sync::Arc<dyn OAuthClientDiscovery>>,
73    /// Managed identities cached by provider instance and protected from API mutations.
74    pub cached_trusted_clients: std::collections::HashSet<String>,
75    /// Operator decision for assigning client-credentials scopes.
76    pub client_credentials_scope_privileges: Option<std::sync::Arc<dyn OAuthClientPrivileges>>,
77    pub client_id_generator: Option<std::sync::Arc<dyn OAuthClientIdGenerator>>,
78    pub client_secret_generator: Option<std::sync::Arc<dyn OAuthClientSecretGenerator>>,
79    pub client_secret_hasher: Option<std::sync::Arc<dyn OAuthClientSecretHasher>>,
80    /// Application token hashing for authorization codes, access and refresh tokens.
81    pub token_hasher: Option<std::sync::Arc<dyn OAuthTokenHasher>>,
82    pub client_secret_cipher: Option<std::sync::Arc<dyn OAuthClientSecretCipher>>,
83    /// Wire prefix matching Better Auth's `prefix.clientSecret`; stored credentials exclude it.
84    pub client_secret_prefix: Option<String>,
85    pub login_page: String,
86    pub consent_page: String,
87    pub select_account_page: Option<String>,
88    pub code_expires_in: f64,
89    /// User access-token lifetime in seconds, including refresh grant issuance.
90    pub access_token_expires_in: f64,
91    /// Access-token lifetime for issuance without a user.
92    pub m2m_access_token_expires_in: f64,
93    pub id_token_expires_in: f64,
94    pub refresh_token_expires_in: f64,
95    /// Per-scope access-expiration ceilings. Numeric values are absolute Unix
96    /// seconds (and numeric zero is ignored), matching the reference policy.
97    pub scope_expirations: std::collections::HashMap<String, crate::jwt::JwtExpiration>,
98    /// Maximum assertion future expiration and issued-at age, in seconds (default 300).
99    pub assertion_max_lifetime: f64,
100    pub refresh_token_reuse_interval: f64,
101    pub allow_public_client_prelogin: bool,
102    pub enforce_per_client_resources: bool,
103    pub advertised_scopes: Option<Vec<String>>,
104    pub advertised_claims: Option<Vec<String>>,
105    pub grant_types: Vec<String>,
106    pub allow_dynamic_client_registration: bool,
107    pub allow_unauthenticated_client_registration: bool,
108    pub disable_jwt_plugin: bool,
109    pub pairwise_secret: Option<String>,
110    pub dpop_signing_algorithms: Option<Vec<String>>,
111    pub dpop_proof_max_age_seconds: Option<f64>,
112}
113impl Default for OAuthProviderConfig {
114    fn default() -> Self {
115        Self {
116            client_discoveries: Vec::new(),
117            cached_trusted_clients: std::collections::HashSet::new(),
118            client_credentials_scope_privileges: None,
119            client_id_generator: None,
120            client_secret_generator: None,
121            client_secret_hasher: None,
122            client_secret_cipher: None,
123            token_hasher: None,
124            client_secret_prefix: None,
125            login_page: "/login".into(),
126            consent_page: "/consent".into(),
127            select_account_page: None,
128            code_expires_in: 600.0,
129            access_token_expires_in: 3600.0,
130            m2m_access_token_expires_in: 3600.0,
131            id_token_expires_in: 36_000.0,
132            refresh_token_expires_in: 2_592_000.0,
133            scope_expirations: std::collections::HashMap::new(),
134            assertion_max_lifetime: 300.0,
135            refresh_token_reuse_interval: 0.0,
136            allow_public_client_prelogin: false,
137            enforce_per_client_resources: true,
138            scopes: ["openid", "profile", "email", "offline_access"]
139                .map(str::to_owned)
140                .into(),
141            advertised_scopes: None,
142            advertised_claims: None,
143            grant_types: ["authorization_code", "client_credentials", "refresh_token"]
144                .map(str::to_owned)
145                .into(),
146            allow_dynamic_client_registration: false,
147            allow_unauthenticated_client_registration: false,
148            disable_jwt_plugin: false,
149            pairwise_secret: None,
150            dpop_signing_algorithms: None,
151            dpop_proof_max_age_seconds: None,
152        }
153    }
154}
155#[derive(Clone, Debug, Default)]
156pub struct OAuthProviderPlugin {
157    config: OAuthProviderConfig,
158    client_jwks: std::sync::Arc<client_jwks::Cache>,
159    trusted_clients: std::sync::Arc<discovery::TrustedCache>,
160}
161impl OAuthProviderPlugin {
162    #[must_use]
163    pub fn new() -> Self {
164        Self::default()
165    }
166    #[must_use]
167    pub fn with_config(mut config: OAuthProviderConfig) -> Self {
168        config.scopes = unique(
169            config
170                .scopes
171                .into_iter()
172                .filter(|scope| !scope.is_empty())
173                .collect(),
174        );
175        Self {
176            config,
177            ..Self::default()
178        }
179    }
180    /// Return the issuer's authorization-server configuration for trusted server callers.
181    #[must_use]
182    pub fn get_oauth_server_config_endpoint() -> ServerEndpoint<Value> {
183        ServerEndpoint::new("oauth-provider", "getOAuthServerConfig")
184    }
185    /// Return `OpenID` configuration when the provider supports the openid scope.
186    #[must_use]
187    pub fn get_open_id_config_endpoint() -> ServerEndpoint<Value> {
188        ServerEndpoint::new("oauth-provider", "getOpenIdConfig")
189    }
190    fn metadata<S: AuthSchema>(&self, ctx: &AuthContext<S>) -> AuthResult<Value> {
191        let jwt = if self.config.disable_jwt_plugin {
192            None
193        } else {
194            Some(
195                ctx.extensions
196                    .get::<super::jwt::JwtDiscoveryOptions>()
197                    .ok_or_else(|| AuthError::config("jwt_config"))?,
198            )
199        };
200        let base = format!(
201            "{}{}",
202            ctx.config.base_url.trim_end_matches('/'),
203            ctx.config.base_path
204        );
205        let issuer = jwt
206            .as_ref()
207            .and_then(|jwt| jwt.issuer.as_deref())
208            .unwrap_or(&base);
209        let issuer = normalize_issuer(issuer);
210        let grants = unique(self.config.grant_types.clone());
211        let mut token_auth = vec![
212            "client_secret_basic",
213            "client_secret_post",
214            "private_key_jwt",
215        ];
216        if self.config.allow_unauthenticated_client_registration
217            || !self.config.client_discoveries.is_empty()
218        {
219            token_auth.insert(0, "none");
220        }
221        let signing = [
222            "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512",
223            "EdDSA",
224        ];
225        let mut output: serde_json::Map<String, Value> = serde_json::from_value(json!({
226            "scopes_supported":self.config.advertised_scopes.as_ref().unwrap_or(&self.config.scopes),
227            "issuer":issuer,"authorization_endpoint":format!("{base}/oauth2/authorize"),"token_endpoint":format!("{base}/oauth2/token"),
228            "introspection_endpoint":format!("{base}/oauth2/introspect"),"revocation_endpoint":format!("{base}/oauth2/revoke"),
229            "response_types_supported":if grants.iter().any(|grant|grant=="authorization_code"){vec!["code"]}else{vec![]},
230            "response_modes_supported":["query"],"grant_types_supported":grants,
231            "token_endpoint_auth_methods_supported":token_auth,"token_endpoint_auth_signing_alg_values_supported":signing,
232            "introspection_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","private_key_jwt"],
233            "introspection_endpoint_auth_signing_alg_values_supported":signing,
234            "revocation_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post","private_key_jwt"],
235            "revocation_endpoint_auth_signing_alg_values_supported":signing,
236            "code_challenge_methods_supported":["S256"],"authorization_response_iss_parameter_supported":true,
237            "dpop_signing_alg_values_supported":self.config.dpop_signing_algorithms.clone().unwrap_or_else(||["EdDSA","ES256","ES512","PS256","RS256"].map(str::to_owned).into()),
238            "backchannel_logout_supported":!self.config.disable_jwt_plugin,"backchannel_logout_session_supported":!self.config.disable_jwt_plugin,
239        }))?;
240        if let Some(jwt) = jwt.as_ref() {
241            _ = output.insert(
242                "jwks_uri".into(),
243                json!(
244                    jwt.remote_url
245                        .clone()
246                        .unwrap_or_else(|| format!("{base}{}", jwt.jwks_path))
247                ),
248            );
249        }
250        if self.config.allow_dynamic_client_registration {
251            _ = output.insert(
252                "registration_endpoint".into(),
253                json!(format!("{base}/oauth2/register")),
254            );
255        }
256        if self.config.scopes.iter().any(|scope| scope == "openid") {
257            let mut claims = ["sub", "iss", "aud", "exp", "iat", "sid", "scope", "azp"]
258                .map(str::to_owned)
259                .to_vec();
260            for (scope, names) in [
261                (
262                    "profile",
263                    vec!["name", "picture", "given_name", "family_name"],
264                ),
265                ("email", vec!["email", "email_verified"]),
266            ] {
267                if self.config.scopes.iter().any(|value| value == scope) {
268                    claims.extend(names.into_iter().map(str::to_owned));
269                }
270            }
271            _ = output.insert(
272                "claims_supported".into(),
273                json!(self.config.advertised_claims.as_ref().unwrap_or(&claims)),
274            );
275            _ = output.insert("claims_parameter_supported".into(), json!(true));
276            _ = output.insert(
277                "userinfo_endpoint".into(),
278                json!(format!("{base}/oauth2/userinfo")),
279            );
280            _ = output.insert(
281                "subject_types_supported".into(),
282                json!(if self
283                    .config
284                    .pairwise_secret
285                    .as_ref()
286                    .is_some_and(|secret| !secret.is_empty())
287                {
288                    vec!["public", "pairwise"]
289                } else {
290                    vec!["public"]
291                }),
292            );
293            _ = output.insert("acr_values_supported".into(), json!(["0"]));
294            _ = output.insert(
295                "id_token_signing_alg_values_supported".into(),
296                json!(jwt.as_ref().map_or_else(
297                    || vec!["HS256".to_owned()],
298                    |jwt| jwt.signing_algorithms.clone()
299                )),
300            );
301            _ = output.insert(
302                "end_session_endpoint".into(),
303                json!(format!("{base}/oauth2/end-session")),
304            );
305            _ = output.insert("request_parameter_supported".into(), json!(false));
306            _ = output.insert("request_uri_parameter_supported".into(), json!(false));
307            _ = output.insert(
308                "prompt_values_supported".into(),
309                json!(["login", "consent", "create", "select_account", "none"]),
310            );
311        }
312        let mut contributed = serde_json::Map::new();
313        for discovery in &self.config.client_discoveries {
314            contributed.extend(discovery.discovery_metadata());
315        }
316        contributed.extend(output);
317        Ok(Value::Object(contributed))
318    }
319}
320fn unique(values: Vec<String>) -> Vec<String> {
321    let mut result = Vec::new();
322    for value in values {
323        if !result.contains(&value) {
324            result.push(value);
325        }
326    }
327    result
328}
329fn normalize_issuer(value: &str) -> String {
330    let Ok(mut url) = url::Url::parse(value) else {
331        return value.into();
332    };
333    let loopback = url.host_str().is_some_and(|host| {
334        host == "localhost" || host == "127.0.0.1" || host == "[::1]" || host == "::1"
335    });
336    if url.scheme() != "https" && !loopback {
337        _ = url.set_scheme("https");
338    }
339    url.set_query(None);
340    url.set_fragment(None);
341    url.to_string().trim_end_matches('/').to_owned()
342}
343#[async_trait]
344impl<S: AuthSchema> AuthPlugin<S> for OAuthProviderPlugin {
345    fn name(&self) -> &'static str {
346        "oauth-provider"
347    }
348    fn allowed_media_types(&self, route: &AuthRoute) -> Vec<&'static str> {
349        match route.path.as_str() {
350            "/oauth2/end-session/confirm"
351            | "/oauth2/token"
352            | "/oauth2/introspect"
353            | "/oauth2/revoke"
354            | "/oauth2/userinfo" => vec!["application/x-www-form-urlencoded"],
355            "/oauth2/end-session" => vec!["application/json", "application/x-www-form-urlencoded"],
356            _ => vec!["application/json"],
357        }
358    }
359    fn routes(&self) -> Vec<AuthRoute> {
360        vec![
361            AuthRoute::post(
362                "/oauth2/end-session/confirm",
363                "oauth2EndSessionConfirmation",
364            ),
365            AuthRoute::get("/oauth2/end-session", "oauth2EndSession"),
366            AuthRoute::post("/oauth2/end-session", "oauth2EndSession"),
367            AuthRoute::get("/oauth2/userinfo", "oauth2UserInfo"),
368            AuthRoute::post("/oauth2/userinfo", "oauth2UserInfo"),
369            AuthRoute::post("/oauth2/revoke", "oauth2Revoke"),
370            AuthRoute::post("/oauth2/introspect", "oauth2Introspect"),
371            AuthRoute::post("/oauth2/token", "oauth2Token"),
372            AuthRoute::post("/oauth2/create-client", "createOAuthClient"),
373            AuthRoute::get("/oauth2/get-client", "getOAuthClient"),
374            AuthRoute::get("/oauth2/public-client", "getOAuthClientPublic"),
375            AuthRoute::post(
376                "/oauth2/public-client-prelogin",
377                "getOAuthClientPublicPrelogin",
378            ),
379            AuthRoute::get("/oauth2/get-clients", "getOAuthClients"),
380            AuthRoute::post("/oauth2/delete-client", "deleteOAuthClient"),
381            AuthRoute::post("/oauth2/update-client", "updateOAuthClient"),
382            AuthRoute::post("/oauth2/register", "registerOAuthClient"),
383            AuthRoute::post("/oauth2/consent", "oauth2Consent"),
384            AuthRoute::post("/oauth2/continue", "oauth2Continue"),
385            AuthRoute::get("/oauth2/get-consents", "getOAuthConsents"),
386            AuthRoute::get("/oauth2/get-consent", "getOAuthConsent"),
387            AuthRoute::post("/oauth2/update-consent", "updateOAuthConsent"),
388            AuthRoute::post("/oauth2/delete-consent", "deleteOAuthConsent"),
389            AuthRoute::get("/oauth2/authorize", "oauth2Authorize"),
390            AuthRoute::post("/oauth2/authorize", "oauth2Authorize"),
391            AuthRoute::post("/oauth2/client/rotate-secret", "rotateClientSecret"),
392        ]
393    }
394    fn server_endpoints(&self) -> Vec<EndpointDefinition> {
395        vec![
396            crate::endpoint::definition(
397                "oauth2EndSessionConfirmation",
398                "oauth2EndSessionConfirmation",
399                Some("/oauth2/end-session/confirm"),
400                HttpMethod::Post,
401            ),
402            crate::endpoint::definition(
403                "oauth2EndSession",
404                "oauth2EndSession",
405                Some("/oauth2/end-session"),
406                HttpMethod::Get,
407            ),
408            crate::endpoint::definition(
409                "oauth2UserInfo",
410                "oauth2UserInfo",
411                Some("/oauth2/userinfo"),
412                HttpMethod::Get,
413            ),
414            crate::endpoint::definition(
415                "oauth2Revoke",
416                "oauth2Revoke",
417                Some("/oauth2/revoke"),
418                HttpMethod::Post,
419            ),
420            crate::endpoint::definition(
421                "oauth2Introspect",
422                "oauth2Introspect",
423                Some("/oauth2/introspect"),
424                HttpMethod::Post,
425            ),
426            crate::endpoint::definition(
427                "oauth2Token",
428                "oauth2Token",
429                Some("/oauth2/token"),
430                HttpMethod::Post,
431            ),
432            crate::endpoint::definition(
433                "oauth2Continue",
434                "oauth2Continue",
435                Some("/oauth2/continue"),
436                HttpMethod::Post,
437            ),
438            crate::endpoint::definition(
439                "adminUnlinkClientResource",
440                "adminUnlinkClientResource",
441                Some("/admin/oauth2/resources/:identifier/clients/:client_id"),
442                HttpMethod::Delete,
443            ),
444            crate::endpoint::definition(
445                "adminLinkClientResource",
446                "adminLinkClientResource",
447                Some("/admin/oauth2/resources/:identifier/clients/:client_id"),
448                HttpMethod::Post,
449            ),
450            crate::endpoint::definition(
451                "adminDeleteOAuthResource",
452                "adminDeleteOAuthResource",
453                Some("/admin/oauth2/resources/:identifier"),
454                HttpMethod::Delete,
455            ),
456            crate::endpoint::definition(
457                "adminUpdateOAuthResource",
458                "adminUpdateOAuthResource",
459                Some("/admin/oauth2/resources/:identifier"),
460                HttpMethod::Patch,
461            ),
462            crate::endpoint::definition(
463                "adminGetOAuthResource",
464                "adminGetOAuthResource",
465                Some("/admin/oauth2/resources/:identifier"),
466                HttpMethod::Get,
467            ),
468            crate::endpoint::definition(
469                "adminListOAuthResources",
470                "adminListOAuthResources",
471                Some("/admin/oauth2/resources"),
472                HttpMethod::Get,
473            ),
474            crate::endpoint::definition(
475                "adminCreateOAuthResource",
476                "adminCreateOAuthResource",
477                Some("/admin/oauth2/resources"),
478                HttpMethod::Post,
479            ),
480            crate::endpoint::definition(
481                "deleteOAuthConsent",
482                "deleteOAuthConsent",
483                Some("/oauth2/delete-consent"),
484                HttpMethod::Post,
485            ),
486            crate::endpoint::definition(
487                "updateOAuthConsent",
488                "updateOAuthConsent",
489                Some("/oauth2/update-consent"),
490                HttpMethod::Post,
491            ),
492            crate::endpoint::definition(
493                "getOAuthConsent",
494                "getOAuthConsent",
495                Some("/oauth2/get-consent"),
496                HttpMethod::Get,
497            ),
498            crate::endpoint::definition(
499                "getOAuthConsents",
500                "getOAuthConsents",
501                Some("/oauth2/get-consents"),
502                HttpMethod::Get,
503            ),
504            crate::endpoint::definition(
505                "oauth2Consent",
506                "oauth2Consent",
507                Some("/oauth2/consent"),
508                HttpMethod::Post,
509            ),
510            crate::endpoint::definition(
511                "getOAuthClientPublicPrelogin",
512                "getOAuthClientPublicPrelogin",
513                Some("/oauth2/public-client-prelogin"),
514                HttpMethod::Post,
515            ),
516            crate::endpoint::definition(
517                "oauth2Authorize",
518                "oauth2Authorize",
519                Some("/oauth2/authorize"),
520                HttpMethod::Get,
521            ),
522            crate::endpoint::definition(
523                "adminUpdateOAuthClient",
524                "adminUpdateOAuthClient",
525                Some("/admin/oauth2/update-client"),
526                HttpMethod::Patch,
527            ),
528            crate::endpoint::definition(
529                "adminCreateOAuthClient",
530                "adminCreateOAuthClient",
531                Some("/admin/oauth2/create-client"),
532                HttpMethod::Post,
533            ),
534            crate::endpoint::definition(
535                "registerOAuthClient",
536                "registerOAuthClient",
537                Some("/oauth2/register"),
538                HttpMethod::Post,
539            ),
540            crate::endpoint::definition(
541                "updateOAuthClient",
542                "updateOAuthClient",
543                Some("/oauth2/update-client"),
544                HttpMethod::Post,
545            ),
546            crate::endpoint::definition(
547                "rotateClientSecret",
548                "rotateClientSecret",
549                Some("/oauth2/client/rotate-secret"),
550                HttpMethod::Post,
551            ),
552            crate::endpoint::definition(
553                "deleteOAuthClient",
554                "deleteOAuthClient",
555                Some("/oauth2/delete-client"),
556                HttpMethod::Post,
557            ),
558            crate::endpoint::definition(
559                "getOAuthClients",
560                "getOAuthClients",
561                Some("/oauth2/get-clients"),
562                HttpMethod::Get,
563            ),
564            crate::endpoint::definition(
565                "getOAuthClientPublic",
566                "getOAuthClientPublic",
567                Some("/oauth2/public-client"),
568                HttpMethod::Get,
569            ),
570            crate::endpoint::definition(
571                "getOAuthClient",
572                "getOAuthClient",
573                Some("/oauth2/get-client"),
574                HttpMethod::Get,
575            ),
576            crate::endpoint::definition(
577                "createOAuthClient",
578                "createOAuthClient",
579                Some("/oauth2/create-client"),
580                HttpMethod::Post,
581            ),
582            crate::endpoint::definition(
583                "getOAuthServerConfig",
584                "getOAuthServerConfig",
585                Some("/.well-known/oauth-authorization-server"),
586                HttpMethod::Get,
587            ),
588            crate::endpoint::definition(
589                "getOpenIdConfig",
590                "getOpenIdConfig",
591                Some("/.well-known/openid-configuration"),
592                HttpMethod::Get,
593            ),
594        ]
595    }
596    fn validate_endpoint(
597        &self,
598        call: &EndpointCall,
599        _ctx: &AuthContext<S>,
600    ) -> AuthResult<EndpointInput> {
601        Ok(EndpointInput {
602            body: if call.operation_id() == "oauth2EndSessionConfirmation" {
603                Some(crate::endpoint::validate_fields(
604                    call.body(),
605                    "body",
606                    &[crate::authentication_helpers::JsonField {
607                        name: "action",
608                        kind: crate::authentication_helpers::JsonFieldKind::OneOf(&["confirm"]),
609                        required: true,
610                    }],
611                )?)
612            } else if call.operation_id() == "oauth2EndSession" {
613                logout::validate(call.body(), "body")?
614            } else if call.operation_id() == "oauth2UserInfo" {
615                call.body()
616                    .map(|body| {
617                        crate::endpoint::validate_fields(
618                            Some(body),
619                            "body",
620                            &[crate::authentication_helpers::JsonField::string(
621                                "access_token",
622                                false,
623                            )],
624                        )
625                    })
626                    .transpose()?
627            } else if call.operation_id() == "oauth2Continue" {
628                Some(continuation::validate(call.body())?)
629            } else if call.operation_id() == "adminUpdateOAuthResource" {
630                Some(resources::validate_update(call.body())?)
631            } else if call.operation_id() == "adminCreateOAuthResource" {
632                Some(resources::validate_create(call.body())?)
633            } else if call.operation_id() == "deleteOAuthConsent" {
634                Some(consent::validate_id(call.body(), "body")?)
635            } else if call.operation_id() == "updateOAuthConsent" {
636                Some(consent::validate_update(call.body())?)
637            } else if call.operation_id() == "oauth2Consent" {
638                Some(consent::validate(call.body())?)
639            } else if call.operation_id() == "getOAuthClientPublicPrelogin" {
640                Some(clients::validate_prelogin(call.body())?)
641            } else if call.operation_id() == "adminUpdateOAuthClient" {
642                Some(clients::validate_admin_update(call.body())?)
643            } else if call.operation_id() == "adminCreateOAuthClient" {
644                Some(clients::validate_admin_create(call.body())?)
645            } else if call.operation_id() == "updateOAuthClient" {
646                Some(clients::validate_update(call.body())?)
647            } else if matches!(
648                call.operation_id(),
649                "deleteOAuthClient" | "rotateClientSecret"
650            ) {
651                Some(clients::validate_delete(call.body())?)
652            } else if call.operation_id() == "createOAuthClient" {
653                Some(clients::validate(call.body())?)
654            } else {
655                call.body().cloned()
656            },
657            query: if call.operation_id() == "oauth2EndSession" {
658                logout::validate(call.query(), "query")?
659            } else if call.operation_id() == "getOAuthConsent" {
660                Some(consent::validate_id(call.query(), "query")?)
661            } else if matches!(
662                call.operation_id(),
663                "getOAuthClient" | "getOAuthClientPublic"
664            ) {
665                Some(clients::validate_read(call.query())?)
666            } else {
667                call.query().cloned()
668            },
669        })
670    }
671    async fn on_endpoint(
672        &self,
673        call: &EndpointCall,
674        ctx: &AuthContext<S>,
675    ) -> AuthResult<EndpointResponse> {
676        if call.operation_id() == "oauth2EndSessionConfirmation" {
677            return Box::pin(self.confirm_logout(call, ctx)).await;
678        }
679        if call.operation_id() == "oauth2EndSession" {
680            return Box::pin(self.end_session(call.query(), call.body(), call, ctx)).await;
681        }
682        if call.operation_id() == "oauth2UserInfo" {
683            return Box::pin(self.user_info(call.body(), call, ctx)).await;
684        }
685        if call.operation_id() == "oauth2Revoke" {
686            return Box::pin(self.revoke(call.body(), call, ctx)).await;
687        }
688        if call.operation_id() == "oauth2Introspect" {
689            return Box::pin(self.introspect(call.body(), call, ctx)).await;
690        }
691        if call.operation_id() == "oauth2Token" {
692            return Box::pin(self.token(call.body(), call, ctx)).await;
693        }
694        if call.operation_id() == "oauth2Continue" {
695            return Box::pin(self.continue_authorization(call.body_as()?, call, ctx)).await;
696        }
697        if call.operation_id() == "adminUnlinkClientResource" {
698            return Box::pin(self.unlink_resource(call.params(), call, ctx)).await;
699        }
700        if call.operation_id() == "adminLinkClientResource" {
701            return Box::pin(self.link_resource(call.params(), call, ctx)).await;
702        }
703        if call.operation_id() == "adminDeleteOAuthResource" {
704            return Box::pin(self.delete_resource(call.params(), call, ctx)).await;
705        }
706        if call.operation_id() == "adminUpdateOAuthResource" {
707            return Box::pin(self.update_resource(call.params(), call.body_as()?, call, ctx)).await;
708        }
709        if call.operation_id() == "adminGetOAuthResource" {
710            return Box::pin(self.read_resource(call.params(), call, ctx)).await;
711        }
712        if call.operation_id() == "adminListOAuthResources" {
713            return Box::pin(self.list_resources(call, ctx)).await;
714        }
715        if call.operation_id() == "adminCreateOAuthResource" {
716            return Box::pin(self.create_resource(call.body_as()?, call, ctx)).await;
717        }
718        if call.operation_id() == "deleteOAuthConsent" {
719            return Box::pin(self.delete_consent(call.body_as()?, call, ctx)).await;
720        }
721        if call.operation_id() == "updateOAuthConsent" {
722            return Box::pin(self.update_consent(call.body_as()?, call, ctx)).await;
723        }
724        if call.operation_id() == "getOAuthConsent" {
725            return Box::pin(self.read_consent(call.query_as()?, call, ctx)).await;
726        }
727        if call.operation_id() == "getOAuthConsents" {
728            return Box::pin(self.list_consents(call, ctx)).await;
729        }
730        if call.operation_id() == "oauth2Consent" {
731            return Box::pin(self.consent(call.body_as()?, call, ctx)).await;
732        }
733        if call.operation_id() == "getOAuthClientPublicPrelogin" {
734            return Box::pin(self.read_prelogin_client(call.body_as()?, ctx)).await;
735        }
736        if call.operation_id() == "oauth2Authorize" {
737            let value = if call.method() == Some(&HttpMethod::Post) {
738                call.body()
739            } else {
740                call.query()
741            };
742            return Box::pin(self.authorize(value.cloned(), call, ctx)).await;
743        }
744        if call.operation_id() == "adminUpdateOAuthClient" {
745            return Box::pin(self.update_client(call.body_as()?, call, ctx, true)).await;
746        }
747        if call.operation_id() == "adminCreateOAuthClient" {
748            return Box::pin(self.admin_create_client(call.body_as()?, call, ctx)).await;
749        }
750        if call.operation_id() == "registerOAuthClient" {
751            return Box::pin(self.register_client(call.body(), call, ctx)).await;
752        }
753        if call.operation_id() == "updateOAuthClient" {
754            return Box::pin(self.update_client(call.body_as()?, call, ctx, false)).await;
755        }
756        if call.operation_id() == "rotateClientSecret" {
757            return Box::pin(self.rotate_client_secret(call.body_as()?, call, ctx)).await;
758        }
759        if call.operation_id() == "deleteOAuthClient" {
760            return Box::pin(self.delete_client(call.body_as()?, call, ctx)).await;
761        }
762        if call.operation_id() == "getOAuthClients" {
763            return Box::pin(self.list_clients(call, ctx)).await;
764        }
765        if call.operation_id() == "getOAuthClientPublic" {
766            return Box::pin(self.read_public_client(call.query_as()?, call, ctx)).await;
767        }
768        if call.operation_id() == "getOAuthClient" {
769            return Box::pin(self.read_client(call.query_as()?, call, ctx)).await;
770        }
771        if call.operation_id() == "createOAuthClient" {
772            return Box::pin(self.create_client(call.body_as()?, call, ctx)).await;
773        }
774        if call.operation_id() == "getOpenIdConfig"
775            && !self.config.scopes.iter().any(|scope| scope == "openid")
776        {
777            return Ok(EndpointResponse::error(AuthError::Api {
778                status: 404,
779                code: None,
780                message: String::new(),
781            })
782            .without_error_body()
783            .with_header("Content-Type", "application/json"));
784        }
785        EndpointResponse::json(&self.metadata(ctx)?)
786    }
787    async fn on_request(
788        &self,
789        req: &AuthRequest,
790        ctx: &AuthContext<S>,
791    ) -> AuthResult<Option<AuthResponse>> {
792        if req.path() == "/oauth2/end-session/confirm" && *req.method() == HttpMethod::Post {
793            let action = url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
794                .find(|(key, _)| key == "action")
795                .map(|(_, value)| value.into_owned());
796            if action.as_deref() != Some("confirm") {
797                return Ok(Some(
798                    AuthError::bad_request("Invalid logout action").to_auth_response(),
799                ));
800            }
801            return Ok(Some(
802                Box::pin(self.confirm_logout(req, ctx))
803                    .await?
804                    .to_response()?,
805            ));
806        }
807        if req.path() == "/oauth2/end-session"
808            && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
809        {
810            let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
811            let body = if *req.method() == HttpMethod::Post {
812                Some(alibi_core::utils::json::parse_value(
813                    &serde_json::to_string(
814                        &url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
815                            .into_owned()
816                            .collect::<std::collections::HashMap<_, _>>(),
817                    )?,
818                )?)
819            } else {
820                None
821            };
822            return Ok(Some(
823                Box::pin(self.end_session(Some(&query), body.as_ref(), req, ctx))
824                    .await?
825                    .to_response()?,
826            ));
827        }
828        if req.path() == "/oauth2/userinfo"
829            && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
830        {
831            let body = if *req.method() == HttpMethod::Post {
832                let fields = url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
833                    .map(|(key, value)| {
834                        (
835                            key.into_owned(),
836                            alibi_core::utils::json::JsValue::String(value.into_owned()),
837                        )
838                    })
839                    .collect();
840                Some(alibi_core::utils::json::JsValue::Object(fields))
841            } else {
842                None
843            };
844            return Ok(Some(
845                Box::pin(self.user_info(body.as_ref(), req, ctx))
846                    .await?
847                    .to_response()?,
848            ));
849        }
850        if matches!(
851            req.path(),
852            "/oauth2/token" | "/oauth2/introspect" | "/oauth2/revoke"
853        ) && *req.method() == HttpMethod::Post
854        {
855            let mut fields = indexmap::IndexMap::new();
856            for (key, value) in url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
857            {
858                _ = fields.insert(
859                    key.into_owned(),
860                    alibi_core::utils::json::JsValue::String(value.into_owned()),
861                );
862            }
863            if req.path() == "/oauth2/revoke" {
864                return Ok(Some(
865                    Box::pin(self.revoke(
866                        Some(&alibi_core::utils::json::JsValue::Object(fields)),
867                        req,
868                        ctx,
869                    ))
870                    .await?
871                    .to_response()?,
872                ));
873            }
874            if req.path() == "/oauth2/introspect" {
875                return Ok(Some(
876                    Box::pin(self.introspect(
877                        Some(&alibi_core::utils::json::JsValue::Object(fields)),
878                        req,
879                        ctx,
880                    ))
881                    .await?
882                    .to_response()?,
883                ));
884            }
885            return Ok(Some(
886                Box::pin(self.token(
887                    Some(&alibi_core::utils::json::JsValue::Object(fields)),
888                    req,
889                    ctx,
890                ))
891                .await?
892                .to_response()?,
893            ));
894        }
895        if req.path() == "/oauth2/authorize"
896            && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
897        {
898            let value = if *req.method() == HttpMethod::Post {
899                let mut fields = indexmap::IndexMap::new();
900                for (name, value) in
901                    url::form_urlencoded::parse(req.body.as_deref().unwrap_or_default())
902                {
903                    _ = fields.insert(
904                        name.into_owned(),
905                        alibi_core::utils::json::JsValue::String(value.into_owned()),
906                    );
907                }
908                alibi_core::utils::json::JsValue::Object(fields)
909            } else {
910                alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?
911            };
912            return Ok(Some(
913                Box::pin(self.authorize(Some(value), req, ctx))
914                    .await?
915                    .to_response()?,
916            ));
917        }
918        if req.path() == "/oauth2/register" && *req.method() == HttpMethod::Post {
919            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
920            return Ok(Some(
921                Box::pin(self.register_client(Some(&body), req, ctx))
922                    .await?
923                    .to_response()?,
924            ));
925        }
926        if req.path() == "/oauth2/update-client" && *req.method() == HttpMethod::Post {
927            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
928            let input =
929                alibi_core::utils::json::from_value(clients::validate_update(Some(&body))?)?;
930            return Ok(Some(
931                Box::pin(self.update_client(input, req, ctx, false))
932                    .await?
933                    .to_response()?,
934            ));
935        }
936        if matches!(
937            req.path(),
938            "/oauth2/delete-client" | "/oauth2/client/rotate-secret"
939        ) && *req.method() == HttpMethod::Post
940        {
941            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
942            let input =
943                alibi_core::utils::json::from_value(clients::validate_delete(Some(&body))?)?;
944            if req.path() == "/oauth2/client/rotate-secret" {
945                return Ok(Some(
946                    Box::pin(self.rotate_client_secret(input, req, ctx))
947                        .await?
948                        .to_response()?,
949                ));
950            }
951            return Ok(Some(
952                Box::pin(self.delete_client(input, req, ctx))
953                    .await?
954                    .to_response()?,
955            ));
956        }
957        if req.path() == "/oauth2/public-client-prelogin" && *req.method() == HttpMethod::Post {
958            let body = req.body_as_json()?;
959            let input =
960                alibi_core::utils::json::from_value(clients::validate_prelogin(Some(&body))?)?;
961            return Ok(Some(
962                Box::pin(self.read_prelogin_client(input, ctx))
963                    .await?
964                    .to_response()?,
965            ));
966        }
967        if req.path() == "/oauth2/continue" && *req.method() == HttpMethod::Post {
968            let body = req.body_as_json()?;
969            let input = alibi_core::utils::json::from_value(continuation::validate(Some(&body))?)?;
970            return Ok(Some(
971                Box::pin(self.continue_authorization(input, req, ctx))
972                    .await?
973                    .to_response()?,
974            ));
975        }
976        if req.path() == "/oauth2/consent" && *req.method() == HttpMethod::Post {
977            let body = req.body_as_json()?;
978            let input = alibi_core::utils::json::from_value(consent::validate(Some(&body))?)?;
979            return Ok(Some(
980                Box::pin(self.consent(input, req, ctx))
981                    .await?
982                    .to_response()?,
983            ));
984        }
985        if req.path() == "/oauth2/delete-consent" && *req.method() == HttpMethod::Post {
986            let body = req.body_as_json()?;
987            let input =
988                alibi_core::utils::json::from_value(consent::validate_id(Some(&body), "body")?)?;
989            return Ok(Some(
990                Box::pin(self.delete_consent(input, req, ctx))
991                    .await?
992                    .to_response()?,
993            ));
994        }
995        if req.path() == "/oauth2/update-consent" && *req.method() == HttpMethod::Post {
996            let body = req.body_as_json()?;
997            let input =
998                alibi_core::utils::json::from_value(consent::validate_update(Some(&body))?)?;
999            return Ok(Some(
1000                Box::pin(self.update_consent(input, req, ctx))
1001                    .await?
1002                    .to_response()?,
1003            ));
1004        }
1005        if req.path() == "/oauth2/get-consent" && *req.method() == HttpMethod::Get {
1006            let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
1007            let input =
1008                alibi_core::utils::json::from_value(consent::validate_id(Some(&query), "query")?)?;
1009            return Ok(Some(
1010                Box::pin(self.read_consent(input, req, ctx))
1011                    .await?
1012                    .to_response()?,
1013            ));
1014        }
1015        if req.path() == "/oauth2/get-consents" && *req.method() == HttpMethod::Get {
1016            return Ok(Some(
1017                Box::pin(self.list_consents(req, ctx))
1018                    .await?
1019                    .to_response()?,
1020            ));
1021        }
1022        if req.path() == "/oauth2/get-clients" && *req.method() == HttpMethod::Get {
1023            return Ok(Some(
1024                Box::pin(self.list_clients(req, ctx)).await?.to_response()?,
1025            ));
1026        }
1027        if matches!(req.path(), "/oauth2/get-client" | "/oauth2/public-client")
1028            && *req.method() == HttpMethod::Get
1029        {
1030            let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
1031            let input = alibi_core::utils::json::from_value(clients::validate_read(Some(&query))?)?;
1032            if req.path() == "/oauth2/public-client" {
1033                return Ok(Some(
1034                    Box::pin(self.read_public_client(input, req, ctx))
1035                        .await?
1036                        .to_response()?,
1037                ));
1038            }
1039            return Ok(Some(
1040                Box::pin(self.read_client(input, req, ctx))
1041                    .await?
1042                    .to_response()?,
1043            ));
1044        }
1045        if req.path() == "/oauth2/create-client" && *req.method() == HttpMethod::Post {
1046            let body: alibi_core::utils::json::JsValue = req.body_as_json()?;
1047            let input = alibi_core::utils::json::from_value(clients::validate(Some(&body))?)?;
1048            return Ok(Some(
1049                Box::pin(self.create_client(input, req, ctx))
1050                    .await?
1051                    .to_response()?,
1052            ));
1053        }
1054        Ok(None)
1055    }
1056    async fn on_init(&self, ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
1057        if self
1058            .config
1059            .advertised_scopes
1060            .as_ref()
1061            .is_some_and(|scopes| {
1062                scopes
1063                    .iter()
1064                    .any(|scope| !self.config.scopes.contains(scope))
1065            })
1066        {
1067            return Err(AuthError::config(
1068                "advertisedMetadata.scopes_supported contains an unsupported scope",
1069            ));
1070        }
1071        if self
1072            .config
1073            .pairwise_secret
1074            .as_ref()
1075            .is_some_and(|secret| !secret.is_empty() && secret.len() < 32)
1076        {
1077            return Err(AuthError::config(
1078                "pairwiseSecret must be at least 32 characters long for adequate HMAC-SHA256 security",
1079            ));
1080        }
1081        if self
1082            .config
1083            .grant_types
1084            .iter()
1085            .any(|grant| grant == "refresh_token")
1086            && !self
1087                .config
1088                .grant_types
1089                .iter()
1090                .any(|grant| grant == "authorization_code")
1091        {
1092            return Err(AuthError::config(
1093                "refresh_token grant requires authorization_code grant",
1094            ));
1095        }
1096        if self.config.disable_jwt_plugin && self.config.client_secret_hasher.is_some() {
1097            return Err(AuthError::config(
1098                "unable to store hashed secrets because id tokens will be signed with secret",
1099            ));
1100        }
1101        if !self.config.disable_jwt_plugin && self.config.client_secret_cipher.is_some() {
1102            return Err(AuthError::config(
1103                "encryption method not recommended, please use 'hashed' or the 'hash' function",
1104            ));
1105        }
1106        let mut discovery_ids = std::collections::HashSet::new();
1107        for discovery in &self.config.client_discoveries {
1108            if alibi_core::utils::javascript::trim(discovery.id()).is_empty() {
1109                return Err(AuthError::config(
1110                    "OAuth Provider extension client discovery id cannot be empty",
1111                ));
1112            }
1113            if !discovery_ids.insert(discovery.id()) {
1114                return Err(AuthError::config(format!(
1115                    "OAuth Provider extensions register client discovery id \"{}\" more than once. Extension contributions must be disjoint.",
1116                    discovery.id()
1117                )));
1118            }
1119        }
1120        ctx.extensions.insert(discovery::TrustedClients {
1121            ids: self.config.cached_trusted_clients.clone(),
1122            cache: self.trusted_clients.clone(),
1123        });
1124        ctx.extensions.insert(discovery::ClientDiscoveries(
1125            self.config.client_discoveries.clone(),
1126        ));
1127        ctx.register_session_deletion_hook(std::sync::Arc::new(
1128            session_retirement::TokenRetirementHook,
1129        ));
1130        Ok(())
1131    }
1132}