1mod additional_fields;
3mod callback;
4mod callback_id_token;
5mod configured;
6mod discovery;
7mod domain_confirmation;
8mod domain_verification;
9mod domains;
10mod encryption_algorithms;
11pub(crate) mod endpoint_host;
12pub use encryption_algorithms::SsoSamlEncryptionPolicy;
13mod hooks;
14mod idp_metadata_validation;
15mod idp_metadata_xml;
16mod idp_services;
17mod mapping_schema;
18mod mutation_guard;
19mod oidc_scopes;
20mod user_resolution;
21pub use mutation_guard::{
22 SsoMutationAction, SsoMutationGuard, SsoMutationProvider, SsoProviderMutation,
23 SsoProviderMutationGuard,
24};
25pub use user_resolution::{
26 SsoAccountKey, SsoProtocol, SsoResolvedProfile, SsoResolver, SsoUserResolution,
27 SsoUserResolutionInput, SsoUserResolver,
28};
29mod organization_provisioning;
30mod saml_signout;
31pub use organization_provisioning::{
32 SsoOrganizationProvisioning, SsoOrganizationRoleResolver, SsoRoleResolver,
33};
34mod private_key;
35pub use private_key::{SsoKeyResolver, SsoPrivateKey, SsoPrivateKeyContext, SsoPrivateKeyResolver};
36mod provider_limit;
37pub use provider_limit::{SsoProviderLimit, SsoProviderLimitResolver};
38mod provisioning;
39pub use provisioning::{SsoProvisioner, SsoProvisioningData, SsoUserProvisioner};
40mod provider_delete;
41mod provider_list;
42mod provider_read;
43mod provider_update;
44mod provider_update_saml;
45mod registration_saml;
46mod saml_acs;
47mod saml_crypto;
48mod signing_algorithms;
49pub use signing_algorithms::{SsoDeprecatedAlgorithmBehavior, SsoSamlSigningPolicy};
50mod saml_decryption;
51mod saml_logout;
52mod saml_sign_in;
53mod saml_slo;
54mod sign_in;
55mod sp_metadata;
56mod sp_metadata_xml;
57use alibi_core::endpoint::{
58 EndpointCall, EndpointDefinition, EndpointInput, EndpointResponse, ServerEndpoint,
59};
60use alibi_core::session::SessionRequest;
61use alibi_core::store::SsoProvider;
62use alibi_core::utils::json::JsValue;
63use alibi_core::{
64 AuthContext, AuthError, AuthPlugin, AuthRequest, AuthResponse, AuthResult, AuthRoute,
65 AuthSchema, AuthUser, HttpMethod,
66};
67use async_trait::async_trait;
68use serde_json::{Value, json};
69#[derive(Clone, Debug, Default)]
71pub struct SsoDefaultProvider {
72 pub provider_id: String,
73 pub domain: String,
74 pub oidc_config: Option<Value>,
75 pub saml_config: Option<Value>,
76 pub private_key: Option<SsoPrivateKey>,
77}
78#[derive(Clone, Debug)]
79pub struct SsoConfig {
80 pub default_providers: Vec<SsoDefaultProvider>,
81 pub resolve_private_key: Option<SsoKeyResolver>,
82 pub provision_user: Option<SsoProvisioner>,
83 pub guard_provider_mutation: Option<SsoMutationGuard>,
84 pub resolve_user: Option<SsoResolver>,
85 pub provision_user_on_every_login: bool,
86 pub organization_provisioning: SsoOrganizationProvisioning,
87 pub providers_limit: usize,
88 pub resolve_providers_limit: Option<SsoProviderLimit>,
90 pub domain_verification: bool,
91 pub domain_verification_dns_servers: Vec<std::net::SocketAddr>,
93 pub domain_verification_token_prefix: String,
94 pub default_override_user_info: bool,
95 pub trust_email_verified: bool,
96 pub disable_implicit_sign_up: bool,
97 pub redirect_uri: Option<String>,
98 pub saml_enable_in_response_to_validation: bool,
99 pub saml_allow_idp_initiated: bool,
100 pub saml_signing_policy: SsoSamlSigningPolicy,
101 pub saml_encryption_policy: SsoSamlEncryptionPolicy,
102 pub saml_idp_initiated_callback_url: Option<String>,
103 pub saml_request_ttl: chrono::Duration,
104 pub saml_clock_skew: chrono::Duration,
105 pub saml_require_timestamps: bool,
106 pub saml_max_response_size: usize,
107 pub saml_max_metadata_size: usize,
108 pub saml_enable_single_logout: bool,
109 pub saml_want_logout_request_signed: bool,
110 pub saml_want_logout_response_signed: bool,
111 pub saml_logout_request_ttl: chrono::Duration,
112}
113impl Default for SsoConfig {
114 fn default() -> Self {
115 Self {
116 default_providers: Vec::new(),
117 resolve_private_key: None,
118 provision_user: None,
119 guard_provider_mutation: None,
120 resolve_user: None,
121 provision_user_on_every_login: false,
122 organization_provisioning: SsoOrganizationProvisioning::default(),
123 providers_limit: 10,
124 resolve_providers_limit: None,
125 domain_verification: false,
126 domain_verification_dns_servers: Vec::new(),
127 domain_verification_token_prefix: "better-auth-token".into(),
128 default_override_user_info: false,
129 trust_email_verified: false,
130 disable_implicit_sign_up: false,
131 redirect_uri: None,
132 saml_enable_in_response_to_validation: true,
133 saml_allow_idp_initiated: false,
134 saml_signing_policy: SsoSamlSigningPolicy::default(),
135 saml_encryption_policy: SsoSamlEncryptionPolicy::default(),
136 saml_idp_initiated_callback_url: None,
137 saml_request_ttl: chrono::Duration::minutes(5),
138 saml_clock_skew: chrono::Duration::minutes(5),
139 saml_require_timestamps: false,
140 saml_max_response_size: 262_144,
141 saml_max_metadata_size: 102_400,
142 saml_enable_single_logout: false,
143 saml_want_logout_request_signed: false,
144 saml_want_logout_response_signed: false,
145 saml_logout_request_ttl: chrono::Duration::minutes(5),
146 }
147 }
148}
149#[derive(Clone, Debug, Default)]
150pub struct SsoPlugin {
151 config: SsoConfig,
152}
153impl SsoPlugin {
154 #[must_use]
155 pub fn new() -> Self {
156 Self::default()
157 }
158 #[must_use]
159 pub fn with_config(config: SsoConfig) -> Self {
160 Self { config }
161 }
162 pub fn register_sso_provider_endpoint(input: &Value) -> AuthResult<ServerEndpoint<Value>> {
163 ServerEndpoint::new("sso", "registerSSOProvider").with_body(input)
164 }
165 pub fn callback_sso_shared_endpoint(query: &Value) -> AuthResult<ServerEndpoint<Value>> {
166 ServerEndpoint::new("sso", "callbackSSOShared").with_query(query)
167 }
168 pub(super) fn oidc_redirect_uri(&self, base: &str, provider_id: &str) -> String {
169 self.config
170 .redirect_uri
171 .as_ref()
172 .filter(|uri| !uri.trim().is_empty())
173 .map_or_else(
174 || format!("{base}/sso/callback/{provider_id}"),
175 |uri| {
176 if url::Url::parse(uri).is_ok() {
177 uri.clone()
178 } else {
179 format!("{base}/{}", uri.trim_start_matches('/'))
180 }
181 },
182 )
183 }
184 async fn register<S: AuthSchema>(
185 &self,
186 input: &JsValue,
187 request: &impl SessionRequest,
188 ctx: &AuthContext<S>,
189 ) -> AuthResult<EndpointResponse> {
190 let (user, _) = super::helpers::ordinary_session(request, ctx).await?;
191 let dynamic_limit = match &self.config.resolve_providers_limit {
192 Some(resolver) => Some(
193 resolver
194 .0
195 .limit(alibi_core::wire::UserView::from(&user))
196 .await?,
197 ),
198 None => None,
199 };
200 if dynamic_limit.map_or(self.config.providers_limit == 0, |limit| limit == 0.0) {
201 return Err(api(403, "SSO provider registration is disabled"));
202 }
203 let count = ctx
204 .database
205 .list_sso_providers_by_user(user.id().as_ref())
206 .await?
207 .len();
208 if dynamic_limit.map_or(count >= self.config.providers_limit, |limit| {
209 provider_limit::reached(count, limit)
210 }) {
211 return Err(api(
212 403,
213 "You have reached the maximum number of SSO providers",
214 ));
215 }
216 let extras = additional_fields::parse(
217 &ctx.config.advanced.database.sso_provider_fields,
218 input,
219 true,
220 )?;
221 let body = input.to_json_value()?;
222 let provider_id = required_string(&body, "providerId")?;
223 let issuer = required_string(&body, "issuer")?;
224 let domain = required_string(&body, "domain")?;
225 if let Some(organization_id) = body.get("organizationId").and_then(Value::as_str) {
226 let member = ctx
227 .database
228 .get_member(organization_id, user.id().as_ref())
229 .await?
230 .ok_or_else(|| api(400, "You are not a member of the organization"))?;
231 if ctx
232 .get_metadata(super::organization::METADATA_ENABLED)
233 .is_some()
234 && !member
235 .role
236 .split(',')
237 .any(|r| matches!(r.trim(), "owner" | "admin"))
238 {
239 return Err(api(
240 403,
241 "You must be an organization owner or admin to register SSO providers",
242 ));
243 }
244 }
245 if [
246 "credential",
247 "email-otp",
248 "magic-link",
249 "phone-number",
250 "anonymous",
251 "siwe",
252 ]
253 .contains(&provider_id)
254 || self
255 .config
256 .default_providers
257 .iter()
258 .any(|provider| provider.provider_id == provider_id)
259 || ctx
260 .config
261 .account
262 .account_linking
263 .trusted_providers
264 .iter()
265 .any(|p| p == provider_id)
266 {
267 return Err(api(
268 422,
269 "This providerId is reserved and cannot be used for an SSO provider",
270 ));
271 }
272 if ctx.database.get_sso_provider(provider_id).await?.is_some() {
273 return Err(api(422, "SSO provider with this providerId already exists"));
274 }
275 let oidc_input = if let Some(config) = body.get("oidcConfig") {
276 discovery::validate_endpoints(config, &|url| {
277 ctx.config.is_redirect_target_trusted(url)
278 })?;
279 Some(
280 if config.get("skipDiscovery").and_then(Value::as_bool) == Some(true) {
281 config.clone()
282 } else {
283 discovery::resolve(config, issuer, ctx).await?
284 },
285 )
286 } else {
287 None
288 };
289 let oidc=oidc_input.as_ref().map(|config| {
290 let method=config.get("tokenEndpointAuthentication").and_then(Value::as_str).unwrap_or("client_secret_basic");
291 if method!="private_key_jwt" && config.get("clientSecret").and_then(Value::as_str).is_none_or(str::is_empty) {
292 return Err(api(400,"clientSecret is required when using client_secret_basic or client_secret_post authentication"));
293 }
294 if method=="private_key_jwt" && !self.private_key_source_configured(provider_id) {return Err(api(400,"private_key_jwt authentication requires either a resolvePrivateKey callback or a privateKey in defaultSSO"));}
295 let mut output=serde_json::Map::new();
296 for field in ["clientId","clientSecret","authorizationEndpoint","tokenEndpoint","privateKeyId","privateKeyAlgorithm","jwksEndpoint","pkce","mapping","scopes","userInfoEndpoint"] {
297 if let Some(value)=config.get(field) { _=output.insert(field.into(),value.clone()); }
298 }
299 _=output.insert("pkce".into(),config.get("pkce").cloned().unwrap_or_else(||json!(true)));
300 let stored_issuer = if config.get("skipDiscovery").and_then(Value::as_bool)==Some(true) {issuer} else {config.get("issuer").and_then(Value::as_str).unwrap_or(issuer)};
301 _=output.insert("issuer".into(),json!(stored_issuer));
302 _=output.insert("tokenEndpointAuthentication".into(),json!(method));
303 _=output.insert("discoveryEndpoint".into(),config.get("discoveryEndpoint").cloned().unwrap_or_else(||json!(format!("{issuer}/.well-known/openid-configuration"))));
304 _=output.insert("overrideUserInfo".into(),json!(body.get("overrideUserInfo").and_then(Value::as_bool).unwrap_or(false)||self.config.default_override_user_info));
305 serde_json::to_string(&output).map_err(Into::into)
306 }).transpose()?;
307 let saml = body
308 .get("samlConfig")
309 .map(|config| {
310 signing_algorithms::validate(config, &self.config.saml_signing_policy)?;
311 registration_saml::prepare(config, issuer, self.config.saml_max_metadata_size)
312 })
313 .transpose()?;
314 let provider = ctx
315 .database
316 .create_sso_provider(SsoProvider {
317 id: uuid::Uuid::new_v4().to_string(),
318 issuer: issuer.into(),
319 domain: domain.into(),
320 provider_id: provider_id.into(),
321 user_id: Some(user.id().into_owned()),
322 organization_id: body
323 .get("organizationId")
324 .and_then(Value::as_str)
325 .map(str::to_owned),
326 oidc_config: oidc,
327 saml_config: saml,
328 domain_verified: self.config.domain_verification.then_some(false),
329 additional_fields: extras.values,
330 })
331 .await?;
332 let mut output = serde_json::to_value(&provider)?;
333 additional_fields::filter(
334 &ctx.config.advanced.database.sso_provider_fields,
335 &mut output,
336 );
337 let object = output
338 .as_object_mut()
339 .ok_or_else(|| AuthError::internal("SSO provider serialization failed"))?;
340 if self.config.domain_verification {
341 let token = self.issue_domain_proof(provider_id, ctx).await?;
342 _ = object.insert("domainVerificationToken".into(), json!(token));
343 } else {
344 _ = object.remove("domainVerified");
345 }
346 for (key, value) in [
347 ("oidcConfig", provider.oidc_config.as_deref()),
348 ("samlConfig", provider.saml_config.as_deref()),
349 ] {
350 _ = object.insert(
351 key.into(),
352 value
353 .map(serde_json::from_str::<Value>)
354 .transpose()?
355 .unwrap_or(Value::Null),
356 );
357 }
358 let base = format!(
359 "{}{}",
360 ctx.config.base_url.trim_end_matches('/'),
361 ctx.config.base_path.trim_end_matches('/')
362 );
363 let redirect = self
364 .config
365 .redirect_uri
366 .as_ref()
367 .filter(|uri| !uri.trim().is_empty())
368 .map_or_else(
369 || format!("{base}/sso/callback/{provider_id}"),
370 |uri| {
371 if url::Url::parse(uri).is_ok() {
372 uri.clone()
373 } else {
374 format!("{base}/{}", uri.trim_start_matches('/'))
375 }
376 },
377 );
378 _ = object.insert("redirectURI".into(), json!(redirect));
379 EndpointResponse::json(&output)
380 }
381}
382fn provider_email_verified(value: Option<&Value>) -> bool {
383 value.is_some_and(|value| value.as_bool() == Some(true) || value.as_str() == Some("true"))
384}
385fn required_string<'a>(value: &'a Value, name: &str) -> AuthResult<&'a str> {
386 value
387 .get(name)
388 .and_then(Value::as_str)
389 .ok_or_else(|| super::endpoint::validation(format!("Invalid {name}")))
390}
391fn api(status: u16, message: &str) -> AuthError {
392 AuthError::Api {
393 status,
394 code: None,
395 message: message.into(),
396 }
397}
398fn validate(input: Option<&JsValue>) -> AuthResult<JsValue> {
399 use super::authentication_helpers::{JsonField, JsonFieldKind};
400 let mut output = super::endpoint::validate_fields(
401 input,
402 "body",
403 &[
404 JsonField::string("providerId", true),
405 JsonField::string("issuer", true),
406 JsonField::string("domain", true),
407 JsonField {
408 name: "oidcConfig",
409 kind: JsonFieldKind::Record,
410 required: false,
411 },
412 JsonField {
413 name: "samlConfig",
414 kind: JsonFieldKind::Record,
415 required: false,
416 },
417 JsonField::string("organizationId", false),
418 JsonField {
419 name: "overrideUserInfo",
420 kind: JsonFieldKind::Boolean,
421 required: false,
422 },
423 ],
424 )?;
425 if output
426 .get("issuer")
427 .and_then(JsValue::as_str)
428 .is_none_or(|issuer| url::Url::parse(issuer).is_err())
429 {
430 return Err(super::endpoint::validation("[body.issuer] Invalid URL"));
431 }
432 if let Some(config) = output.get("oidcConfig") {
433 _ = super::endpoint::validate_fields(
434 Some(config),
435 "body.oidcConfig",
436 &[
437 JsonField::string("clientId", true),
438 JsonField::string("clientSecret", false),
439 JsonField::string("authorizationEndpoint", false),
440 JsonField::string("tokenEndpoint", false),
441 JsonField::string("jwksEndpoint", false),
442 JsonField::string("discoveryEndpoint", false),
443 JsonField::string("userInfoEndpoint", false),
444 JsonField {
445 name: "skipDiscovery",
446 kind: JsonFieldKind::Boolean,
447 required: false,
448 },
449 JsonField {
450 name: "pkce",
451 kind: JsonFieldKind::Boolean,
452 required: false,
453 },
454 ],
455 )?;
456 if let Some(scopes) = config.get("scopes") {
457 oidc_scopes::validate(scopes)?;
458 }
459 if let Some(mapping) = config.get("mapping") {
460 _ = mapping_schema::oidc(mapping)?;
461 }
462 for field in discovery::ENDPOINT_FIELDS {
463 if let Some(value) = config.get(field).and_then(JsValue::as_str)
464 && url::Url::parse(value).is_err()
465 {
466 return Err(super::endpoint::validation(format!(
467 "[body.oidcConfig.{field}] Invalid URL"
468 )));
469 }
470 }
471 }
472 if let Some(config) = output.get("samlConfig") {
473 let parsed = registration_saml::validate(config)?;
474 if let JsValue::Object(fields) = &mut output {
475 _ = fields.insert("samlConfig".into(), parsed);
476 }
477 }
478 Ok(output)
479}
480#[async_trait]
481impl<S: AuthSchema> AuthPlugin<S> for SsoPlugin {
482 fn name(&self) -> &'static str {
483 "sso"
484 }
485 async fn on_init(&self, ctx: &mut alibi_core::AuthInitContext<S>) -> AuthResult<()> {
486 alibi_core::store::validate_sso_fields(&ctx.config.advanced.database)
487 }
488 fn origin_check_skip_paths(&self) -> Vec<&'static str> {
489 vec!["/sso/saml2/sp/acs", "/sso/saml2/sp/slo"]
490 }
491 fn allowed_media_types(&self, route: &AuthRoute) -> Vec<&'static str> {
492 if route.path.starts_with("/sso/saml2/sp/acs/")
493 || route.path.starts_with("/sso/saml2/sp/slo/")
494 {
495 vec!["application/x-www-form-urlencoded", "application/json"]
496 } else {
497 vec!["application/json"]
498 }
499 }
500 fn routes(&self) -> Vec<AuthRoute> {
501 let mut routes = vec![
502 AuthRoute::get("/sso/saml2/sp/slo/:providerId", "sloEndpoint"),
503 AuthRoute::post("/sso/saml2/sp/slo/:providerId", "sloEndpoint"),
504 AuthRoute::post("/sso/saml2/logout/:providerId", "initiateSLO"),
505 AuthRoute::get("/sso/callback/:providerId", "handleSSOCallback"),
506 AuthRoute::get("/sso/callback", "handleSSOCallbackShared"),
507 AuthRoute::get(
508 "/sso/saml2/sp/acs/:providerId",
509 "handleSAMLAssertionConsumerService",
510 ),
511 AuthRoute::post(
512 "/sso/saml2/sp/acs/:providerId",
513 "handleSAMLAssertionConsumerService",
514 ),
515 AuthRoute::get("/sso/saml2/sp/metadata", "getSSOServiceProviderMetadata"),
516 AuthRoute::post("/sign-in/sso", "signInSSO"),
517 AuthRoute::post("/sso/register", "registerSSOProvider"),
518 AuthRoute::get("/sso/get-provider", "getSSOProvider"),
519 AuthRoute::get("/sso/providers", "listSSOProviders"),
520 AuthRoute::post("/sso/delete-provider", "deleteSSOProvider"),
521 AuthRoute::post("/sso/update-provider", "updateSSOProvider"),
522 ];
523 if self.config.domain_verification {
524 routes.push(AuthRoute::post("/sso/verify-domain", "verifyDomain"));
525 routes.push(AuthRoute::post(
526 "/sso/request-domain-verification",
527 "requestDomainVerification",
528 ));
529 }
530 routes
531 }
532 async fn before_request(
533 &self,
534 request: &AuthRequest,
535 ctx: &AuthContext<S>,
536 ) -> AuthResult<Option<alibi_core::BeforeRequestAction>> {
537 if request.path() == "/sign-out" && self.config.saml_enable_single_logout {
538 self.clear_saml_signout_tracking(request, ctx).await?;
539 }
540 Ok(None)
541 }
542 async fn after_request(
543 &self,
544 request: &AuthRequest,
545 ctx: &AuthContext<S>,
546 response: AuthResponse,
547 ) -> AuthResult<AuthResponse> {
548 if request.path().starts_with("/callback/")
549 && let Some(completed) = request
550 .extensions()
551 .get::<crate::helpers::CompletedSession<S>>()
552 {
553 self.assign_social_domain_organization(completed.user.id().as_ref(), ctx)
554 .await?;
555 }
556 Ok(response)
557 }
558 fn endpoint_hooks(&self) -> Vec<&dyn alibi_core::endpoint::EndpointHook<S>> {
559 vec![self]
560 }
561 fn server_endpoints(&self) -> Vec<EndpointDefinition> {
562 let mut endpoints = vec![
563 super::endpoint::definition(
564 "sloEndpoint",
565 "sloEndpoint",
566 Some("/sso/saml2/sp/slo/:providerId"),
567 HttpMethod::Get,
568 ),
569 super::endpoint::definition(
570 "initiateSLO",
571 "initiateSLO",
572 Some("/sso/saml2/logout/:providerId"),
573 HttpMethod::Post,
574 ),
575 super::endpoint::definition(
576 "acsEndpoint",
577 "handleSAMLAssertionConsumerService",
578 Some("/sso/saml2/sp/acs/:providerId"),
579 HttpMethod::Get,
580 ),
581 super::endpoint::definition(
582 "spMetadata",
583 "getSSOServiceProviderMetadata",
584 Some("/sso/saml2/sp/metadata"),
585 HttpMethod::Get,
586 ),
587 super::endpoint::definition(
588 "callbackSSOShared",
589 "handleSSOCallbackShared",
590 Some("/sso/callback"),
591 HttpMethod::Get,
592 ),
593 super::endpoint::definition(
594 "callbackSSO",
595 "handleSSOCallback",
596 Some("/sso/callback/:providerId"),
597 HttpMethod::Get,
598 ),
599 super::endpoint::definition(
600 "signInSSO",
601 "signInSSO",
602 Some("/sign-in/sso"),
603 HttpMethod::Post,
604 ),
605 super::endpoint::definition(
606 "updateSSOProvider",
607 "updateSSOProvider",
608 Some("/sso/update-provider"),
609 HttpMethod::Post,
610 ),
611 super::endpoint::definition(
612 "deleteSSOProvider",
613 "deleteSSOProvider",
614 Some("/sso/delete-provider"),
615 HttpMethod::Post,
616 ),
617 super::endpoint::definition(
618 "listSSOProviders",
619 "listSSOProviders",
620 Some("/sso/providers"),
621 HttpMethod::Get,
622 ),
623 super::endpoint::definition(
624 "getSSOProvider",
625 "getSSOProvider",
626 Some("/sso/get-provider"),
627 HttpMethod::Get,
628 ),
629 super::endpoint::definition(
630 "registerSSOProvider",
631 "registerSSOProvider",
632 Some("/sso/register"),
633 HttpMethod::Post,
634 ),
635 ];
636 if self.config.domain_verification {
637 endpoints.push(super::endpoint::definition(
638 "verifyDomain",
639 "verifyDomain",
640 Some("/sso/verify-domain"),
641 HttpMethod::Post,
642 ));
643 endpoints.push(super::endpoint::definition(
644 "requestDomainVerification",
645 "requestDomainVerification",
646 Some("/sso/request-domain-verification"),
647 HttpMethod::Post,
648 ));
649 }
650 endpoints
651 }
652 fn validate_endpoint(
653 &self,
654 call: &EndpointCall,
655 ctx: &AuthContext<S>,
656 ) -> AuthResult<EndpointInput> {
657 if call.operation_id() == "sloEndpoint" {
658 return Ok(EndpointInput {
659 body: saml_slo::validate(call.body(), "body")?,
660 query: saml_slo::validate(call.query(), "query")?,
661 });
662 }
663 if call.operation_id() == "initiateSLO" {
664 return Ok(EndpointInput {
665 body: Some(saml_logout::validate(call.body())?),
666 query: call.query().cloned(),
667 });
668 }
669 if call.operation_id() == "handleSAMLAssertionConsumerService" {
670 return Ok(EndpointInput {
671 body: saml_acs::validate_body(call.body())?,
672 query: saml_acs::validate_query(call.query())?,
673 });
674 }
675 if matches!(
676 call.operation_id(),
677 "handleSSOCallback" | "handleSSOCallbackShared"
678 ) {
679 return Ok(EndpointInput {
680 body: None,
681 query: Some(super::endpoint::validate_fields(
682 call.query(),
683 "query",
684 &[
685 super::authentication_helpers::JsonField::string("code", false),
686 super::authentication_helpers::JsonField::string("state", false),
687 super::authentication_helpers::JsonField::string("error", false),
688 super::authentication_helpers::JsonField::string(
689 "error_description",
690 false,
691 ),
692 ],
693 )?),
694 });
695 }
696 if call.operation_id() == "signInSSO" {
697 return Ok(EndpointInput {
698 body: Some(sign_in::validate(call.body())?),
699 query: call.query().cloned(),
700 });
701 }
702 if call.operation_id() == "updateSSOProvider" {
703 return Ok(EndpointInput {
704 body: Some({
705 let mut output = provider_update::validate_update(call.body())?;
706 additional_fields::validate_and_copy(
707 &ctx.config.advanced.database.sso_provider_fields,
708 call.body(),
709 &mut output,
710 false,
711 )?;
712 output
713 }),
714 query: call.query().cloned(),
715 });
716 }
717 if matches!(
718 call.operation_id(),
719 "deleteSSOProvider" | "requestDomainVerification" | "verifyDomain"
720 ) {
721 return Ok(EndpointInput {
722 body: Some(Self::validate_delete(call.body())?),
723 query: call.query().cloned(),
724 });
725 }
726 if call.operation_id() == "listSSOProviders" {
727 return Ok(EndpointInput {
728 body: call.body().cloned(),
729 query: call.query().cloned(),
730 });
731 }
732 if matches!(
733 call.operation_id(),
734 "getSSOProvider" | "getSSOServiceProviderMetadata"
735 ) {
736 return Ok(EndpointInput {
737 body: None,
738 query: Some(provider_read::validate_query(call.query())?),
739 });
740 }
741 Ok(EndpointInput {
742 body: Some({
743 let mut output = validate(call.body())?;
744 additional_fields::validate_and_copy(
745 &ctx.config.advanced.database.sso_provider_fields,
746 call.body(),
747 &mut output,
748 true,
749 )?;
750 output
751 }),
752 query: call.query().cloned(),
753 })
754 }
755 async fn on_endpoint(
756 &self,
757 call: &EndpointCall,
758 ctx: &AuthContext<S>,
759 ) -> AuthResult<EndpointResponse> {
760 if call.operation_id() == "sloEndpoint" {
761 let provider = call
762 .params()
763 .and_then(|params| params.get("providerId"))
764 .and_then(JsValue::as_str)
765 .ok_or_else(|| crate::endpoint::validation("Missing providerId"))?;
766 let body = call.body().map(JsValue::to_json_value).transpose()?;
767 let query = call.query().map(JsValue::to_json_value).transpose()?;
768 return self
769 .slo(provider, body.as_ref(), query.as_ref(), call, ctx)
770 .await;
771 }
772 if call.operation_id() == "initiateSLO" {
773 let provider = call
774 .params()
775 .and_then(|params| params.get("providerId"))
776 .and_then(JsValue::as_str)
777 .ok_or_else(|| crate::endpoint::validation("Missing providerId"))?;
778 return self
779 .initiate_logout(
780 provider,
781 call.body()
782 .ok_or_else(|| crate::endpoint::validation("Missing body"))?,
783 call,
784 ctx,
785 )
786 .await;
787 }
788 if call.operation_id() == "handleSAMLAssertionConsumerService" {
789 let provider = call
790 .params()
791 .and_then(|params| params.get("providerId"))
792 .and_then(JsValue::as_str)
793 .ok_or_else(|| crate::endpoint::validation("Missing providerId"))?;
794 let body = call.body().map(JsValue::to_json_value).transpose()?;
795 let query = call.query().map(JsValue::to_json_value).transpose()?;
796 let meta = call.request().map_or_else(
797 alibi_core::RequestMeta::default,
798 alibi_core::RequestMeta::from_request,
799 );
800 return self
801 .acs(provider, body.as_ref(), query.as_ref(), call, &meta, ctx)
802 .await;
803 }
804 if matches!(
805 call.operation_id(),
806 "handleSSOCallback" | "handleSSOCallbackShared"
807 ) {
808 let provider_id = if call.operation_id() == "handleSSOCallbackShared" {
809 None
810 } else {
811 Some(
812 call.params()
813 .and_then(|params| params.get("providerId"))
814 .and_then(JsValue::as_str)
815 .ok_or_else(|| super::endpoint::validation("Missing providerId"))?,
816 )
817 };
818 let query = call
819 .query()
820 .map(JsValue::to_json_value)
821 .transpose()?
822 .unwrap_or_else(|| json!({}));
823 let meta = call.request().map_or_else(
824 alibi_core::RequestMeta::default,
825 alibi_core::RequestMeta::from_request,
826 );
827 return self.callback(provider_id, &query, call, &meta, ctx).await;
828 }
829 if call.operation_id() == "getSSOServiceProviderMetadata" {
830 return self
831 .metadata(
832 call.query()
833 .ok_or_else(|| crate::endpoint::validation("Missing query"))?,
834 ctx,
835 )
836 .await;
837 }
838 if call.operation_id() == "signInSSO" {
839 return self
840 .sign_in(
841 call.body()
842 .ok_or_else(|| super::endpoint::validation("Missing body"))?,
843 ctx,
844 )
845 .await;
846 }
847 if call.operation_id() == "verifyDomain" {
848 return self
849 .verify_domain(
850 call.body()
851 .ok_or_else(|| super::endpoint::validation("Missing body"))?,
852 call,
853 ctx,
854 )
855 .await;
856 }
857 if call.operation_id() == "requestDomainVerification" {
858 return self
859 .request_domain(
860 call.body()
861 .ok_or_else(|| super::endpoint::validation("Missing body"))?,
862 call,
863 ctx,
864 )
865 .await;
866 }
867 if call.operation_id() == "updateSSOProvider" {
868 return self
869 .update(
870 call.body()
871 .ok_or_else(|| super::endpoint::validation("Missing body"))?,
872 call,
873 ctx,
874 )
875 .await;
876 }
877 if call.operation_id() == "deleteSSOProvider" {
878 return self
879 .delete(
880 call.body()
881 .ok_or_else(|| super::endpoint::validation("Missing body"))?,
882 call,
883 ctx,
884 )
885 .await;
886 }
887 if call.operation_id() == "listSSOProviders" {
888 return self.list(call, ctx).await;
889 }
890 if call.operation_id() == "getSSOProvider" {
891 return self
892 .read(
893 call.query()
894 .ok_or_else(|| super::endpoint::validation("Missing query"))?,
895 call,
896 ctx,
897 )
898 .await;
899 }
900 self.register(
901 call.body()
902 .ok_or_else(|| super::endpoint::validation("Missing body"))?,
903 call,
904 ctx,
905 )
906 .await
907 }
908 async fn on_request(
909 &self,
910 req: &AuthRequest,
911 ctx: &AuthContext<S>,
912 ) -> AuthResult<Option<AuthResponse>> {
913 if let Some(provider) = req.path().strip_prefix("/sso/saml2/sp/slo/")
914 && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
915 && !provider.contains('/')
916 {
917 let body = if let Some(bytes) = req.body.as_deref() {
918 if req
919 .header("content-type")
920 .is_some_and(|type_| type_.starts_with("application/x-www-form-urlencoded"))
921 {
922 Some(serde_json::to_value(
923 url::form_urlencoded::parse(bytes)
924 .into_owned()
925 .collect::<std::collections::HashMap<_, _>>(),
926 )?)
927 } else {
928 Some(serde_json::from_slice::<Value>(bytes)?)
929 }
930 } else {
931 None
932 };
933 return Ok(Some(
934 self.slo(
935 provider,
936 body.as_ref(),
937 Some(&serde_json::to_value(&req.query)?),
938 req,
939 ctx,
940 )
941 .await?
942 .to_response()?,
943 ));
944 }
945 if let Some(provider) = req.path().strip_prefix("/sso/saml2/logout/")
946 && *req.method() == HttpMethod::Post
947 && !provider.contains('/')
948 {
949 let body = req.body_as_json::<JsValue>()?;
950 return Ok(Some(
951 self.initiate_logout(provider, &saml_logout::validate(Some(&body))?, req, ctx)
952 .await?
953 .to_response()?,
954 ));
955 }
956 if let Some(provider) = req.path().strip_prefix("/sso/saml2/sp/acs/")
957 && matches!(req.method(), HttpMethod::Get | HttpMethod::Post)
958 && !provider.contains('/')
959 {
960 let body = if let Some(bytes) = req.body.as_deref() {
961 if req
962 .header("content-type")
963 .is_some_and(|content| content.starts_with("application/x-www-form-urlencoded"))
964 {
965 Some(serde_json::to_value(
966 url::form_urlencoded::parse(bytes)
967 .into_owned()
968 .collect::<std::collections::HashMap<_, _>>(),
969 )?)
970 } else {
971 Some(serde_json::from_slice::<Value>(bytes)?)
972 }
973 } else {
974 None
975 };
976 let body = body
977 .as_ref()
978 .map(|body| alibi_core::utils::json::parse_value(&serde_json::to_string(body)?))
979 .transpose()?;
980 let body = saml_acs::validate_body(body.as_ref())?
981 .map(|body| body.to_json_value())
982 .transpose()?;
983 let query = serde_json::to_value(&req.query)?;
984 return Ok(Some(
985 self.acs(
986 provider,
987 body.as_ref(),
988 Some(&query),
989 req,
990 &alibi_core::RequestMeta::from_request(req),
991 ctx,
992 )
993 .await?
994 .to_response()?,
995 ));
996 }
997 if req.path() == "/sso/saml2/sp/metadata" && *req.method() == HttpMethod::Get {
998 let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
999 return Ok(Some(
1000 self.metadata(&provider_read::validate_query(Some(&query))?, ctx)
1001 .await?
1002 .to_response()?,
1003 ));
1004 }
1005 if req.path() == "/sso/callback" && *req.method() == HttpMethod::Get {
1006 return Ok(Some(
1007 self.callback(
1008 None,
1009 &serde_json::to_value(&req.query)?,
1010 req,
1011 &alibi_core::RequestMeta::from_request(req),
1012 ctx,
1013 )
1014 .await?
1015 .to_response()?,
1016 ));
1017 }
1018 if let Some(provider_id) = req.path().strip_prefix("/sso/callback/")
1019 && *req.method() == HttpMethod::Get
1020 && !provider_id.contains('/')
1021 {
1022 return Ok(Some(
1023 self.callback(
1024 Some(provider_id),
1025 &serde_json::to_value(&req.query)?,
1026 req,
1027 &alibi_core::RequestMeta::from_request(req),
1028 ctx,
1029 )
1030 .await?
1031 .to_response()?,
1032 ));
1033 }
1034 if req.path() == "/sign-in/sso" && *req.method() == HttpMethod::Post {
1035 let input = alibi_core::utils::json::parse_value(
1036 std::str::from_utf8(req.body.as_deref().unwrap_or_default())
1037 .map_err(|_| super::endpoint::validation("Invalid JSON"))?,
1038 )?;
1039 return Ok(Some(
1040 self.sign_in(&sign_in::validate(Some(&input))?, ctx)
1041 .await?
1042 .to_response()?,
1043 ));
1044 }
1045 if self.config.domain_verification
1046 && req.path() == "/sso/verify-domain"
1047 && *req.method() == HttpMethod::Post
1048 {
1049 let input = alibi_core::utils::json::parse_value(
1050 std::str::from_utf8(req.body.as_deref().unwrap_or_default())
1051 .map_err(|_| super::endpoint::validation("Invalid JSON"))?,
1052 )?;
1053 return Ok(Some(
1054 self.verify_domain(&Self::validate_delete(Some(&input))?, req, ctx)
1055 .await?
1056 .to_response()?,
1057 ));
1058 }
1059 if self.config.domain_verification
1060 && req.path() == "/sso/request-domain-verification"
1061 && *req.method() == HttpMethod::Post
1062 {
1063 let input = alibi_core::utils::json::parse_value(
1064 std::str::from_utf8(req.body.as_deref().unwrap_or_default())
1065 .map_err(|_| super::endpoint::validation("Invalid JSON"))?,
1066 )?;
1067 return Ok(Some(
1068 self.request_domain(&Self::validate_delete(Some(&input))?, req, ctx)
1069 .await?
1070 .to_response()?,
1071 ));
1072 }
1073 if req.path() == "/sso/update-provider" && *req.method() == HttpMethod::Post {
1074 let input = alibi_core::utils::json::parse_value(
1075 std::str::from_utf8(req.body.as_deref().unwrap_or_default())
1076 .map_err(|_| super::endpoint::validation("Invalid JSON"))?,
1077 )?;
1078 return Ok(Some(
1079 self.update(&provider_update::validate_update(Some(&input))?, req, ctx)
1080 .await?
1081 .to_response()?,
1082 ));
1083 }
1084 if req.path() == "/sso/delete-provider" && *req.method() == HttpMethod::Post {
1085 let input = alibi_core::utils::json::parse_value(
1086 std::str::from_utf8(req.body.as_deref().unwrap_or_default())
1087 .map_err(|_| super::endpoint::validation("Invalid JSON"))?,
1088 )?;
1089 return Ok(Some(
1090 self.delete(&Self::validate_delete(Some(&input))?, req, ctx)
1091 .await?
1092 .to_response()?,
1093 ));
1094 }
1095 if req.path() == "/sso/providers" && *req.method() == HttpMethod::Get {
1096 return Ok(Some(self.list(req, ctx).await?.to_response()?));
1097 }
1098 if req.path() == "/sso/get-provider" && *req.method() == HttpMethod::Get {
1099 let query = alibi_core::utils::json::parse_value(&serde_json::to_string(&req.query)?)?;
1100 return Ok(Some(
1101 self.read(&provider_read::validate_query(Some(&query))?, req, ctx)
1102 .await?
1103 .to_response()?,
1104 ));
1105 }
1106 if req.path() != "/sso/register" || *req.method() != HttpMethod::Post {
1107 return Ok(None);
1108 }
1109 let input = alibi_core::utils::json::parse_value(
1110 std::str::from_utf8(req.body.as_deref().unwrap_or_default())
1111 .map_err(|_| super::endpoint::validation("Invalid JSON"))?,
1112 )?;
1113 Ok(Some(
1114 self.register(&validate(Some(&input))?, req, ctx)
1115 .await?
1116 .to_response()?,
1117 ))
1118 }
1119}