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