1use alibi_core::{AuthResult, AuthSchema, AuthTransaction};
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::{any::Any, sync::Arc};
6#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "lowercase")]
8pub enum SsoProtocol {
9 Oidc,
10 Saml,
11}
12#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(rename_all = "camelCase")]
14pub struct SsoAccountKey {
15 pub issuer: String,
16 pub account_id: String,
17}
18#[derive(Clone, Debug, Serialize, Deserialize)]
19#[serde(rename_all = "camelCase")]
20pub struct SsoUserResolutionInput {
21 pub protocol: SsoProtocol,
22 pub provider_id: String,
23 pub account_key: SsoAccountKey,
24 pub provider_user: Value,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub provider_claims: Option<Value>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub verified_id_token_claims: Option<Value>,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub provider_attributes: Option<Value>,
31 pub provider_reference: Value,
32}
33#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
34#[serde(rename_all = "lowercase")]
35pub enum SsoResolvedProfile {
36 Preserve,
37 Update,
38}
39#[derive(Clone, Debug, Serialize, Deserialize)]
40#[serde(tag = "action", rename_all = "lowercase")]
41pub enum SsoUserResolution {
42 Continue,
43 Link {
44 #[serde(rename = "userId")]
45 user_id: String,
46 profile: SsoResolvedProfile,
47 },
48 Reject {
49 code: String,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 message: Option<String>,
52 },
53}
54impl SsoUserResolution {
55 pub(super) fn valid(&self) -> bool {
56 match self {
57 Self::Continue => true,
58 Self::Link { user_id, .. } => !user_id.trim().is_empty(),
59 Self::Reject { code, .. } => !code.trim().is_empty(),
60 }
61 }
62}
63#[async_trait::async_trait]
64pub trait SsoUserResolver<S: AuthSchema>: Send + Sync {
65 async fn resolve(
66 &self,
67 input: SsoUserResolutionInput,
68 database: &dyn AuthTransaction<S>,
69 ) -> AuthResult<SsoUserResolution>;
70}
71#[derive(Clone)]
72pub struct SsoResolver {
73 callback: Arc<dyn Any + Send + Sync>,
74}
75impl SsoResolver {
76 pub fn new<S: AuthSchema>(callback: Arc<dyn SsoUserResolver<S>>) -> Self {
77 Self {
78 callback: Arc::new(callback),
79 }
80 }
81 pub(super) fn resolve<S: AuthSchema>(&self) -> AuthResult<Arc<dyn SsoUserResolver<S>>> {
82 self.callback
83 .downcast_ref::<Arc<dyn SsoUserResolver<S>>>()
84 .cloned()
85 .ok_or_else(|| {
86 alibi_core::AuthError::config(
87 "SSO user resolver schema does not match the application schema",
88 )
89 })
90 }
91}
92impl std::fmt::Debug for SsoResolver {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 f.debug_struct("SsoResolver").finish_non_exhaustive()
95 }
96}
97
98pub(super) struct Authentication<'a, S: AuthSchema> {
99 pub identity: crate::oauth::handlers::transactional_identity::Identity<'a, S>,
100 pub input: SsoUserResolutionInput,
101 pub provider: &'a alibi_core::store::SsoProvider,
102}
103fn failure() -> alibi_core::AuthError {
104 alibi_core::AuthError::Api {
105 status: 500,
106 code: Some("SSO_USER_RESOLUTION_FAILED".into()),
107 message: "Unable to resolve the SSO user".into(),
108 }
109}
110impl super::SsoPlugin {
111 pub(super) async fn resolved_authentication<S: AuthSchema>(
112 &self,
113 request: Authentication<'_, S>,
114 ) -> AuthResult<crate::oauth::handlers::ProcessOAuthUserResult> {
115 let resolver = self
116 .config
117 .resolve_user
118 .as_ref()
119 .ok_or_else(failure)?
120 .resolve::<S>()?;
121 let identity = request.identity;
122 let ctx = identity.ctx.clone();
123 if ctx.config.session.stateless {
124 return Err(alibi_core::AuthError::Api {
125 status: 501,
126 code: Some("SSO_USER_RESOLUTION_REQUIRES_DATABASE_SESSIONS".into()),
127 message:
128 "SSO user resolution requires database-backed sessions with database fallback"
129 .into(),
130 });
131 }
132 let provider = request.provider.clone();
133 let input = request.input;
134 let verified = self.config.domain_verification;
135 let name = identity.provider.to_owned();
136 let user = identity.user.clone();
137 let profile = identity.profile.clone();
138 let policy = identity.policy.clone();
139 let tokens = identity.tokens.clone();
140 let meta = identity.meta.clone();
141 let disabled = identity.disable_signup;
142 let scoped = ctx.clone();
143 let authenticated = alibi_core::store::transaction_with_policy(
144 ctx.database.as_ref(),
145 alibi_core::store::AfterCommitErrorPolicy::Report,
146 move |database| {
147 Box::pin(async move {
148 if !provider.id.is_empty() {
149 let current = database
150 .lock_sso_provider(&provider.id, &provider.provider_id)
151 .await?
152 .ok_or_else(|| alibi_core::AuthError::Api {
153 status: 409,
154 code: Some("SSO_PROVIDER_CHANGED".into()),
155 message:
156 "SSO provider changed while account linking was in progress"
157 .into(),
158 })?;
159 if super::sign_in::provider_reference(¤t, verified)?
160 != input.provider_reference
161 {
162 return Err(alibi_core::AuthError::Api {
163 status: 409,
164 code: Some("SSO_PROVIDER_CHANGED".into()),
165 message:
166 "SSO provider changed while account linking was in progress"
167 .into(),
168 });
169 }
170 }
171 let decision = resolver.resolve(input, database).await.map_err(|_| {
172 tracing::error!("SSO user resolution failed");
173 failure()
174 })?;
175 if !decision.valid() {
176 return Err(failure());
177 }
178 if let SsoUserResolution::Reject { code, message } = &decision {
179 return Err(alibi_core::AuthError::Api {
180 status: 403,
181 code: Some(code.clone()),
182 message: message.clone().unwrap_or_default(),
183 });
184 }
185 let identity = crate::oauth::handlers::transactional_identity::Identity {
186 provider: &name,
187 user: &user,
188 profile: &profile,
189 policy: &policy,
190 tokens: &tokens,
191 disable_signup: disabled,
192 meta: &meta,
193 ctx: &scoped,
194 };
195 crate::oauth::handlers::transactional_identity::authenticate(
196 &identity, &decision, database,
197 )
198 .await
199 })
200 },
201 )
202 .await?;
203 crate::oauth::handlers::transactional_identity::publish(&ctx, authenticated).await
204 }
205}