1use super::{
3 AuthContext, AuthError, AuthResult, AuthSchema, EndpointResponse, JsValue, ServerEndpoint,
4 SsoPlugin, SsoProvider, Value, api, json,
5};
6use alibi_core::OAuthStateStrategy;
7use base64::Engine as _;
8use sha2::Digest as _;
9impl SsoPlugin {
10 pub fn sign_in_sso_endpoint(input: &Value) -> AuthResult<ServerEndpoint<Value>> {
11 ServerEndpoint::new("sso", "signInSSO").with_body(input)
12 }
13 pub(super) async fn sign_in<S: AuthSchema>(
14 &self,
15 input: &JsValue,
16 ctx: &AuthContext<S>,
17 ) -> AuthResult<EndpointResponse> {
18 let body = input.to_json_value()?;
19 let string = |name: &str| {
20 body.get(name)
21 .and_then(Value::as_str)
22 .filter(|value| !value.is_empty())
23 };
24 let email = string("email");
25 let domain = string("domain").or_else(|| email.and_then(|email| email.split('@').nth(1)));
26 let provider_id = string("providerId");
27 let org_slug = string("organizationSlug");
28 if self.config.default_providers.is_empty()
29 && email.is_none()
30 && domain.is_none()
31 && provider_id.is_none()
32 && org_slug.is_none()
33 {
34 return Err(api(
35 400,
36 "email, organizationSlug, domain or providerId is required",
37 ));
38 }
39 let org_id = if let Some(slug) = org_slug {
40 ctx.database
41 .get_organization_by_slug(slug)
42 .await?
43 .map(|org| org.id)
44 } else {
45 None
46 };
47 if provider_id.is_none() && org_id.is_none() && domain.is_none() {
48 return Err(api(400, "providerId, orgId or domain is required"));
49 }
50 let provider = if let Some(configured) = self.configured_provider(provider_id, domain)? {
51 Some(configured)
52 } else if let Some(id) = provider_id {
53 ctx.database.get_sso_provider(id).await?
54 } else {
55 let providers = ctx.database.list_sso_providers().await?;
56 if let Some(id) = org_id.as_deref() {
57 providers
58 .into_iter()
59 .find(|provider| provider.organization_id.as_deref() == Some(id))
60 } else {
61 providers
62 .iter()
63 .find(|provider| Some(provider.domain.as_str()) == domain)
64 .cloned()
65 .or_else(|| {
66 providers.into_iter().find(|provider| {
67 domain.is_some_and(|domain| {
68 super::domains::matches(domain, &provider.domain)
69 })
70 })
71 })
72 }
73 }
74 .ok_or_else(|| api(404, "No provider found for the issuer"))?;
75 let oidc = provider
76 .oidc_config
77 .as_deref()
78 .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
79 .filter(|value| !value.is_null());
80 let saml = provider
81 .saml_config
82 .as_deref()
83 .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
84 .filter(|value| !value.is_null());
85 if string("providerType") == Some("oidc") && oidc.is_none() {
86 return Err(api(400, "OIDC provider is not configured"));
87 }
88 if string("providerType") == Some("saml") && saml.is_none() {
89 return Err(api(400, "SAML provider is not configured"));
90 }
91 if self.config.domain_verification && provider.domain_verified != Some(true) {
92 return Err(api(401, "Provider domain has not been verified"));
93 }
94 if string("providerType") == Some("saml") || oidc.is_none() {
95 return self
96 .saml_sign_in(
97 &body,
98 &provider,
99 saml.as_ref()
100 .ok_or_else(|| api(400, "Invalid SSO provider"))?,
101 ctx,
102 )
103 .await;
104 }
105 let config = super::discovery::ensure(
106 oidc.as_ref()
107 .ok_or_else(|| api(400, "Invalid SSO provider"))?,
108 &provider.issuer,
109 ctx,
110 )
111 .await?;
112 let endpoint = config
113 .get("authorizationEndpoint")
114 .and_then(Value::as_str)
115 .filter(|value| !value.is_empty())
116 .ok_or_else(|| {
117 api(
118 400,
119 "Invalid OIDC configuration. Authorization URL not found.",
120 )
121 })?;
122 let state = random(32);
123 let verifier = random(128);
124 let callback = string("callbackURL")
125 .filter(|value| !value.is_empty())
126 .unwrap_or(&ctx.config.base_url);
127 let mut payload = crate::oauth::state::OAuthStatePayload::new(
128 callback.into(),
129 verifier.clone(),
130 string("errorCallbackURL").map(str::to_owned),
131 string("newUserCallbackURL").map(str::to_owned),
132 None,
133 body.get("requestSignUp").and_then(Value::as_bool),
134 serde_json::Map::new(),
135 );
136 payload.server_context = Some(
137 json!({"ssoProviderReference":provider_reference(&provider,self.config.domain_verification)?}),
138 );
139 _ = payload
140 .additional_data
141 .insert("oauthState".into(), json!(state));
142 let cookie = issue_state(
143 &payload,
144 &state,
145 &crate::oauth::state::state_cookie_name(&ctx.config),
146 ctx,
147 )
148 .await?;
149 let base = format!(
150 "{}{}",
151 ctx.config.base_url.trim_end_matches('/'),
152 ctx.config.base_path.trim_end_matches('/')
153 );
154 let redirect = self.oidc_redirect_uri(&base, &provider.provider_id);
155 let scopes = body
156 .get("scopes")
157 .or_else(|| config.get("scopes"))
158 .cloned()
159 .unwrap_or_else(|| json!(["openid", "email", "profile", "offline_access"]));
160 let scope = scopes
161 .as_array()
162 .ok_or_else(|| api(400, "Invalid scopes"))?
163 .iter()
164 .filter_map(Value::as_str)
165 .collect::<Vec<_>>()
166 .join(" ");
167 let mut url = url::Url::parse(endpoint).map_err(|_| {
168 api(
169 400,
170 "Invalid OIDC configuration. Authorization URL not found.",
171 )
172 })?;
173 {
174 let mut query = url.query_pairs_mut();
175 _ = query
176 .append_pair("response_type", "code")
177 .append_pair(
178 "client_id",
179 config
180 .get("clientId")
181 .and_then(Value::as_str)
182 .unwrap_or_default(),
183 )
184 .append_pair("state", &state)
185 .append_pair("scope", &scope)
186 .append_pair("redirect_uri", &redirect);
187 if config.get("pkce") == Some(&json!(true)) {
188 _ = query
189 .append_pair("code_challenge_method", "S256")
190 .append_pair(
191 "code_challenge",
192 &base64::engine::general_purpose::URL_SAFE_NO_PAD
193 .encode(sha2::Sha256::digest(verifier.as_bytes())),
194 );
195 }
196 if let Some(hint) = string("loginHint").or(email) {
197 _ = query.append_pair("login_hint", hint);
198 }
199 }
200 Ok(
201 EndpointResponse::json(&json!({"url":url.to_string(),"redirect":true}))?
202 .with_header("Set-Cookie", cookie),
203 )
204 }
205}
206pub(super) fn random(length: usize) -> String {
207 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
208 use rand::RngExt as _;
209 let mut rng = rand::rng();
210 (0..length)
211 .filter_map(|_| {
212 ALPHABET
213 .get(rng.random_range(0..ALPHABET.len()))
214 .copied()
215 .map(char::from)
216 })
217 .collect()
218}
219pub(super) fn provider_reference(
220 provider: &SsoProvider,
221 verified_field: bool,
222) -> AuthResult<Value> {
223 provider_reference_inner(provider, verified_field, true)
224}
225pub(super) fn mutation_provider_reference(
226 provider: &SsoProvider,
227 verified_field: bool,
228) -> AuthResult<Value> {
229 provider_reference_inner(provider, verified_field, false)
230}
231fn provider_reference_inner(
232 provider: &SsoProvider,
233 verified_field: bool,
234 include_empty_organization: bool,
235) -> AuthResult<Value> {
236 let mut configuration = serde_json::Map::new();
237 _ = configuration.insert("domain".into(), json!(provider.domain));
238 _ = configuration.insert("issuer".into(), json!(provider.issuer));
239 if !provider.id.is_empty() && (include_empty_organization || provider.organization_id.is_some())
240 {
241 _ = configuration.insert("organizationId".into(), json!(provider.organization_id));
242 }
243 if verified_field {
244 _ = configuration.insert("domainVerified".into(), json!(provider.domain_verified));
245 }
246 if let Some(raw) = provider.oidc_config.as_deref() {
247 let mut config: Value = serde_json::from_str(raw)?;
248 if let Some(object) = config.as_object_mut() {
249 _ = object.remove("clientSecret");
250 }
251 _ = configuration.insert("oidcConfig".into(), config);
252 }
253 if let Some(raw) = provider.saml_config.as_deref() {
254 let mut config: Value = serde_json::from_str(raw)?;
255 if let Some(object) = config.as_object_mut() {
256 _ = object.remove("privateKey");
257 let idp = object.entry("idpMetadata").or_insert_with(|| json!({}));
258 if let Some(metadata) = idp.as_object_mut() {
259 for key in [
260 "privateKey",
261 "privateKeyPass",
262 "encPrivateKey",
263 "encPrivateKeyPass",
264 ] {
265 _ = metadata.remove(key);
266 }
267 }
268 if let Some(metadata) = object.get_mut("spMetadata").and_then(Value::as_object_mut) {
269 for key in [
270 "privateKey",
271 "privateKeyPass",
272 "encPrivateKey",
273 "encPrivateKeyPass",
274 ] {
275 _ = metadata.remove(key);
276 }
277 }
278 }
279 _ = configuration.insert("samlConfig".into(), config);
280 }
281 let fingerprint = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
282 sha2::Sha256::digest(canonical(&Value::Object(configuration))?.into_bytes()),
283 );
284 let source = if provider.id.is_empty() {
285 json!({"type":"configured"})
286 } else {
287 json!({"type":"persisted","recordId":provider.id})
288 };
289 Ok(
290 json!({"providerId":provider.provider_id,"source":source,"authenticationConfigurationFingerprint":fingerprint}),
291 )
292}
293pub(super) fn validate(input: Option<&JsValue>) -> AuthResult<JsValue> {
294 use super::super::authentication_helpers::{JsonField, JsonFieldKind};
295 let output = super::super::endpoint::validate_fields(
296 input,
297 "body",
298 &[
299 JsonField::string("email", false),
300 JsonField::string("organizationSlug", false),
301 JsonField::string("providerId", false),
302 JsonField::string("domain", false),
303 JsonField::string("callbackURL", true),
304 JsonField::string("errorCallbackURL", false),
305 JsonField::string("newUserCallbackURL", false),
306 JsonField::string("loginHint", false),
307 JsonField {
308 name: "requestSignUp",
309 kind: JsonFieldKind::Boolean,
310 required: false,
311 },
312 JsonField {
313 name: "providerType",
314 kind: JsonFieldKind::OneOf(&["oidc", "saml"]),
315 required: false,
316 },
317 ],
318 )?;
319 let mut values = output
320 .as_object()
321 .cloned()
322 .ok_or_else(|| super::super::endpoint::validation("Invalid body"))?;
323 for field in ["scopes", "additionalParams"] {
324 if let Some(value) = input.and_then(|value| value.get(field)) {
325 _ = values.insert(field.into(), value.clone());
326 }
327 }
328 Ok(JsValue::Object(values))
329}
330
331fn canonical(value: &Value) -> AuthResult<String> {
332 match value {
333 Value::Object(object) => {
334 let sorted: std::collections::BTreeMap<&String, &Value> = object.iter().collect();
335 let pairs = sorted
336 .into_iter()
337 .map(|(key, value)| {
338 Ok(format!(
339 "{}:{}",
340 serde_json::to_string(key)?,
341 canonical(value)?
342 ))
343 })
344 .collect::<AuthResult<Vec<_>>>()?;
345 Ok(format!("{{{}}}", pairs.join(",")))
346 }
347 Value::Array(values) => Ok(format!(
348 "[{}]",
349 values
350 .iter()
351 .map(canonical)
352 .collect::<AuthResult<Vec<_>>>()?
353 .join(",")
354 )),
355 _ => serde_json::to_string(value).map_err(Into::into),
356 }
357}
358
359pub(super) async fn issue_state<S: AuthSchema>(
360 payload: &crate::oauth::state::OAuthStatePayload,
361 state: &str,
362 cookie_name: &str,
363 ctx: &AuthContext<S>,
364) -> AuthResult<String> {
365 match ctx.config.account.store_state_strategy {
366 OAuthStateStrategy::Database => {
367 _ = ctx
368 .verifications()
369 .create(alibi_core::CreateVerification {
370 identifier: crate::oauth::state::state_verification_identifier(state),
371 value: serde_json::to_string(payload)?,
372 expires_at: chrono::Utc::now() + chrono::Duration::minutes(10),
373 })
374 .await?
375 .ok_or_else(|| api(500, "Unable to create verification"))?;
376 alibi_core::utils::cookie_utils::create_cookie(
377 cookie_name,
378 &alibi_core::utils::cookie_utils::sign_cookie_value(
379 state,
380 ctx.config.current_secret(),
381 ),
382 300,
383 &ctx.config,
384 )
385 }
386 OAuthStateStrategy::Cookie => alibi_core::utils::cookie_utils::create_cookie(
387 cookie_name,
388 &crate::oauth::state::create_cookie_state_value(&ctx.config, payload)?,
389 600,
390 &ctx.config,
391 ),
392 OAuthStateStrategy::Automatic => Err(AuthError::config(
393 "OAuth state strategy has not been initialized",
394 )),
395 }
396}