Skip to main content

alibi_plugins/sso/
hooks.rs

1//! SSO lifecycle hooks around ordinary sign-out and social callback completion.
2use super::{
3    AuthContext, AuthResult, AuthSchema, AuthUser, EndpointCall, EndpointResponse, SsoPlugin,
4};
5#[async_trait::async_trait]
6impl<S: AuthSchema> alibi_core::endpoint::EndpointHook<S> for SsoPlugin {
7    fn matches_before(&self, call: &EndpointCall, _: &AuthContext<S>) -> AuthResult<bool> {
8        Ok(call.path() == Some("/sign-out"))
9    }
10    async fn before(
11        &self,
12        call: &EndpointCall,
13        ctx: &AuthContext<S>,
14    ) -> AuthResult<Option<alibi_core::endpoint::BeforeEndpointAction>> {
15        if self.config.saml_enable_single_logout {
16            self.clear_saml_signout_tracking(call, ctx).await?;
17        }
18        Ok(None)
19    }
20    fn matches_after(
21        &self,
22        call: &EndpointCall,
23        _: &AuthContext<S>,
24        _: &EndpointResponse,
25    ) -> AuthResult<bool> {
26        Ok(call
27            .path()
28            .is_some_and(|path| path.starts_with("/callback/")))
29    }
30    async fn after(
31        &self,
32        call: &EndpointCall,
33        ctx: &AuthContext<S>,
34        response: EndpointResponse,
35    ) -> AuthResult<EndpointResponse> {
36        if let Some(completed) = call
37            .extensions()
38            .get::<crate::helpers::CompletedSession<S>>()
39        {
40            self.assign_social_domain_organization(completed.user.id().as_ref(), ctx)
41                .await?;
42        }
43        Ok(response)
44    }
45}
46impl SsoPlugin {
47    pub(super) async fn assign_social_domain_organization<S: AuthSchema>(
48        &self,
49        user_id: &str,
50        ctx: &AuthContext<S>,
51    ) -> AuthResult<()> {
52        if !self.config.domain_verification
53            || self.config.organization_provisioning.disabled
54            || ctx
55                .get_metadata(crate::organization::METADATA_ENABLED)
56                .is_none()
57        {
58            return Ok(());
59        }
60        let Some(user) = ctx.database.get_user_by_id_record(user_id).await? else {
61            return Ok(());
62        };
63        if !user.email_verified() {
64            return Ok(());
65        }
66        let Some(domain) = user.email().and_then(super::domains::email_domain) else {
67            return Ok(());
68        };
69        let mut providers: Vec<_> = ctx
70            .database
71            .list_sso_providers()
72            .await?
73            .into_iter()
74            .filter(|provider| {
75                provider.domain_verified == Some(true)
76                    && provider
77                        .organization_id
78                        .as_deref()
79                        .is_some_and(|id| !id.is_empty())
80                    && super::domains::matches(&domain, &provider.domain)
81            })
82            .collect();
83        let organizations: std::collections::HashSet<_> = providers
84            .iter()
85            .filter_map(|provider| provider.organization_id.as_deref())
86            .collect();
87        if organizations.len() != 1 {
88            return Ok(());
89        }
90        providers.sort_by(|left, right| left.provider_id.cmp(&right.provider_id));
91        if let Some(provider) = providers.first() {
92            self.provision_organization(
93                &ctx.trusted_user_view(&user),
94                provider,
95                ctx,
96                &serde_json::json!({}),
97                None,
98            )
99            .await?;
100        }
101        Ok(())
102    }
103}