1pub 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#[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}
60pub trait OAuthClientIdGenerator: Send + Sync + std::fmt::Debug {
62 fn generate(&self) -> AuthResult<String>;
63}
64pub trait OAuthClientSecretGenerator: Send + Sync + std::fmt::Debug {
66 fn generate(&self) -> AuthResult<String>;
67}
68#[derive(Clone, Debug)]
70pub struct OAuthProviderConfig {
71 pub scopes: Vec<String>,
72 pub client_discoveries: Vec<std::sync::Arc<dyn OAuthClientDiscovery>>,
73 pub cached_trusted_clients: std::collections::HashSet<String>,
75 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 pub token_hasher: Option<std::sync::Arc<dyn OAuthTokenHasher>>,
82 pub client_secret_cipher: Option<std::sync::Arc<dyn OAuthClientSecretCipher>>,
83 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 pub access_token_expires_in: f64,
91 pub m2m_access_token_expires_in: f64,
93 pub id_token_expires_in: f64,
94 pub refresh_token_expires_in: f64,
95 pub scope_expirations: std::collections::HashMap<String, crate::jwt::JwtExpiration>,
98 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 #[must_use]
182 pub fn get_oauth_server_config_endpoint() -> ServerEndpoint<Value> {
183 ServerEndpoint::new("oauth-provider", "getOAuthServerConfig")
184 }
185 #[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}