Skip to main content

alibi_plugins/
oauth_provider.rs

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