Skip to main content

alibi_plugins/
sso.rs

1//! Enterprise single-sign-on provider registration.
2mod 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/// An application-configured SSO provider, equivalent to a `defaultSSO` entry.
70#[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    /// Resolve the registration limit for the authenticated user on each call.
89    pub resolve_providers_limit: Option<SsoProviderLimit>,
90    pub domain_verification: bool,
91    /// Override the system DNS servers, for example for an enterprise DNS zone.
92    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}