1use super::{
3 AuthContext, AuthError, AuthResult, AuthSchema, EndpointResponse, ServerEndpoint, SsoPlugin,
4 Value, api, json,
5};
6use crate::oauth::{
7 OAuthTokenSet, OAuthUserInfo,
8 handlers::{OAuthIdentity, OAuthProcessPolicy, process_oauth_sign_in},
9 state::{OAuthStatePayload, get_cookie, state_cookie_name, state_verification_identifier},
10};
11use alibi_core::utils::cookie_utils::{create_clear_cookie, verify_cookie_value};
12use alibi_core::{OAuthStateStrategy, RequestMeta, session::SessionRequest};
13use std::sync::Arc;
14impl SsoPlugin {
15 pub fn callback_sso_endpoint(
16 provider_id: &str,
17 query: &Value,
18 ) -> AuthResult<ServerEndpoint<Value>> {
19 ServerEndpoint::new("sso", "callbackSSO")
20 .with_params(&json!({"providerId":provider_id}))?
21 .with_query(query)
22 }
23 pub(super) async fn callback<S: AuthSchema>(
24 &self,
25 provider_id: Option<&str>,
26 query: &Value,
27 request: &impl SessionRequest,
28 meta: &RequestMeta,
29 ctx: &AuthContext<S>,
30 ) -> AuthResult<EndpointResponse> {
31 let base = format!(
32 "{}{}",
33 ctx.config.base_url.trim_end_matches('/'),
34 ctx.config.base_path.trim_end_matches('/')
35 );
36 let default_error = format!("{base}/error");
37 let Some(state) = query
38 .get("state")
39 .and_then(Value::as_str)
40 .filter(|s| !s.is_empty())
41 else {
42 return redirect_error(&default_error, "state_not_found", None);
43 };
44 let cookie = get_cookie(request, &state_cookie_name(&ctx.config));
45 let payload: Option<OAuthStatePayload> = match ctx.config.account.store_state_strategy {
46 OAuthStateStrategy::Database => {
47 let identifier = state_verification_identifier(state);
48 let proof = ctx.verifications().find(&identifier).await?;
49 let payload = proof
50 .as_ref()
51 .and_then(|proof| proof.value().ok())
52 .and_then(|value| serde_json::from_str::<OAuthStatePayload>(value).ok());
53 if let Some(payload) = payload.as_ref()
54 && !ctx.config.account.skip_state_cookie_check
55 && cookie
56 .as_deref()
57 .and_then(|cookie| verify_cookie_value(cookie, ctx.config.current_secret()))
58 .as_deref()
59 != Some(state)
60 {
61 return redirect_error(
62 payload.error_url.as_deref().unwrap_or(&default_error),
63 "state_mismatch",
64 None,
65 );
66 }
67 if payload.is_some() && ctx.verifications().consume(&identifier).await?.is_none() {
68 None
69 } else {
70 payload
71 }
72 }
73 OAuthStateStrategy::Cookie => cookie.as_deref().and_then(|cookie| {
74 crate::oauth::state::decode_cookie_state_value(&ctx.config, cookie).ok()
75 }),
76 OAuthStateStrategy::Automatic => None,
77 };
78 let Some(payload) = payload else {
79 return redirect_error(&default_error, "state_mismatch", None);
80 };
81 let clear = create_clear_cookie(&state_cookie_name(&ctx.config), &ctx.config)?;
82 let target = payload
83 .error_url
84 .as_deref()
85 .unwrap_or(&payload.callback_url);
86 let fail =
87 |error: &str, description: Option<&str>| -> AuthResult<EndpointResponse> {
88 Ok(redirect_error(target, error, description)?
89 .with_header("Set-Cookie", clear.clone()))
90 };
91 if payload
92 .additional_data
93 .get("oauthState")
94 .and_then(Value::as_str)
95 != Some(state)
96 {
97 return fail("state_mismatch", None);
98 }
99 if payload.expires_at < chrono::Utc::now().timestamp_millis() {
100 return fail("state_expired", None);
101 }
102 let provider_id = match provider_id {
104 Some(id) => id,
105 None => match payload
106 .server_context
107 .as_ref()
108 .and_then(|context| context.get("ssoProviderReference"))
109 .and_then(|reference| reference.get("providerId"))
110 .and_then(Value::as_str)
111 {
112 Some(id) => id,
113 None => return fail("invalid_state", Some("missing_sso_provider_reference")),
114 },
115 };
116 let code = query
117 .get("code")
118 .and_then(Value::as_str)
119 .filter(|code| !code.is_empty());
120 let error = query
121 .get("error")
122 .and_then(Value::as_str)
123 .filter(|error| !error.is_empty());
124 if code.is_none() || error.is_some() {
125 return fail(
126 error.unwrap_or("invalid_request"),
127 Some(
128 query
129 .get("error_description")
130 .and_then(Value::as_str)
131 .unwrap_or_else(|| error.unwrap_or("authorization_code_not_found")),
132 ),
133 );
134 }
135 let Some(provider) = self.find_provider(provider_id, ctx).await? else {
136 return fail("invalid_provider", Some("provider not found"));
137 };
138 let reference = payload
139 .server_context
140 .as_ref()
141 .and_then(|value| value.get("ssoProviderReference"));
142 if reference.is_none() {
143 return fail("invalid_state", Some("missing_sso_provider_reference"));
144 }
145 if reference
146 != Some(&super::sign_in::provider_reference(
147 &provider,
148 self.config.domain_verification,
149 )?)
150 {
151 return fail(
152 "invalid_state",
153 Some("sso_provider_changed_during_authentication"),
154 );
155 }
156 if self.config.domain_verification && provider.domain_verified != Some(true) {
157 return Err(api(401, "Provider domain has not been verified"));
158 }
159 let Some(config) = provider
160 .oidc_config
161 .as_deref()
162 .and_then(|raw| serde_json::from_str::<Value>(raw).ok())
163 else {
164 return fail("invalid_provider", Some("provider not found"));
165 };
166 let config = match super::discovery::ensure(&config, &provider.issuer, ctx).await {
167 Ok(config) => config,
168 Err(error) => {
169 return fail(
170 "discovery_failed",
171 Some(super::discovery::callback_error_message(&error)),
172 );
173 }
174 };
175 let Some(token_endpoint) = config.get("tokenEndpoint").and_then(Value::as_str) else {
176 return fail("invalid_provider", Some("token_endpoint_not_found"));
177 };
178 let redirect_uri = self.oidc_redirect_uri(&base, provider_id);
179 let mut form = vec![
180 ("grant_type".to_owned(), "authorization_code".to_owned()),
181 ("code".to_owned(), code.unwrap_or_default().to_owned()),
182 ("redirect_uri".to_owned(), redirect_uri),
183 ];
184 if config.get("pkce") == Some(&json!(true)) {
185 form.push(("code_verifier".into(), payload.code_verifier.clone()));
186 }
187 let client_id = config
188 .get("clientId")
189 .and_then(Value::as_str)
190 .unwrap_or_default();
191 let secret = config
192 .get("clientSecret")
193 .and_then(Value::as_str)
194 .unwrap_or_default();
195 let client_assertion = if config
196 .get("tokenEndpointAuthentication")
197 .and_then(Value::as_str)
198 == Some("private_key_jwt")
199 {
200 let mut key = self
201 .config
202 .default_providers
203 .iter()
204 .find(|provider| provider.provider_id == provider_id)
205 .and_then(|provider| provider.private_key.clone());
206 if key.is_none()
207 && let Some(resolver) = &self.config.resolve_private_key
208 {
209 key = resolver
210 .0
211 .resolve(super::SsoPrivateKeyContext {
212 provider_id: provider_id.into(),
213 key_id: config
214 .get("privateKeyId")
215 .and_then(Value::as_str)
216 .map(str::to_owned),
217 issuer: config
218 .get("issuer")
219 .and_then(Value::as_str)
220 .unwrap_or_default()
221 .into(),
222 })
223 .await?;
224 }
225 let Some(key) = key.filter(super::SsoPrivateKey::has_material) else {
226 return fail("invalid_provider", Some("no_private_key_available"));
227 };
228 Some(key.assertion(&config).map_err(AuthError::internal)?)
229 } else {
230 None
231 };
232 let exchange = async {
233 let (client, token_url) = outbound_client(token_endpoint, ctx).await?;
234 let mut grant = client.post(token_url);
235 if let Some(assertion) = client_assertion.as_ref() {
236 let signed_assertion = assertion
237 .0
238 .get_client_assertion(crate::oauth::OAuthClientAssertionContext {
239 client_id: client_id.into(),
240 token_endpoint: token_endpoint.into(),
241 grant_type: crate::oauth::OAuthTokenGrant::AuthorizationCode,
242 })
243 .await
244 .map_err(AuthError::internal)?;
245 form.push(("client_id".into(), client_id.into()));
246 form.push(("client_assertion".into(), signed_assertion));
247 form.push((
248 "client_assertion_type".into(),
249 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer".into(),
250 ));
251 } else if config
252 .get("tokenEndpointAuthentication")
253 .and_then(Value::as_str)
254 == Some("client_secret_post")
255 {
256 form.push(("client_id".into(), client_id.into()));
257 form.push(("client_secret".into(), secret.into()));
258 } else {
259 grant = grant.basic_auth(client_id, Some(secret));
260 }
261 let raw = grant
262 .form(&form)
263 .send()
264 .await
265 .map_err(|e| AuthError::internal(e.to_string()))?
266 .error_for_status()
267 .map_err(|e| AuthError::internal(e.to_string()))?
268 .json::<Value>()
269 .await
270 .map_err(|e| AuthError::internal(e.to_string()))?;
271 let verified = if let Some(token) = raw
272 .get("id_token")
273 .and_then(Value::as_str)
274 .filter(|token| !token.is_empty())
275 {
276 let endpoint = config
277 .get("jwksEndpoint")
278 .and_then(Value::as_str)
279 .ok_or_else(|| api(400, "jwks_endpoint_not_found"))?;
280 Some(
281 super::callback_id_token::verify(
282 token,
283 endpoint,
284 client_id,
285 &provider.issuer,
286 ctx,
287 )
288 .await?,
289 )
290 } else {
291 None
292 };
293 let access = raw
294 .get("access_token")
295 .and_then(Value::as_str)
296 .ok_or_else(|| AuthError::internal("missing_access_token"))?;
297 if self.config.resolve_user.is_some() && verified.is_none() {
298 return Err(api(400, "id_token_required_for_user_resolution"));
299 }
300 let verified_claims = verified.clone();
301 let profile = if let Some(endpoint) = config
302 .get("userInfoEndpoint")
303 .and_then(Value::as_str)
304 .filter(|endpoint| !endpoint.is_empty())
305 {
306 let (client, info_url) = outbound_client(endpoint, ctx).await?;
307 let profile = client
308 .get(info_url)
309 .bearer_auth(access)
310 .send()
311 .await
312 .map_err(|error| AuthError::internal(error.to_string()))?
313 .error_for_status()
314 .map_err(|error| AuthError::internal(error.to_string()))?
315 .json::<Value>()
316 .await
317 .map_err(|error| AuthError::internal(error.to_string()))?;
318 if verified
319 .as_ref()
320 .is_some_and(|claims| profile.get("sub") != claims.get("sub"))
321 {
322 return Err(api(400, "id_token_userinfo_subject_mismatch"));
323 }
324 profile
325 } else {
326 verified.ok_or_else(|| api(400, "user_info_endpoint_not_found"))?
327 };
328 let field = |name: &str, default: &str| {
329 config
330 .get("mapping")
331 .and_then(|mapping| mapping.get(name))
332 .and_then(Value::as_str)
333 .filter(|field| !field.is_empty())
334 .unwrap_or(default)
335 .to_owned()
336 };
337 let text = |name: &str, default: &str| {
338 profile
339 .get(field(name, default))
340 .and_then(Value::as_str)
341 .map(str::to_owned)
342 };
343 let user = OAuthUserInfo {
344 id: text("id", "sub")
345 .filter(|value| !value.is_empty())
346 .ok_or_else(|| AuthError::internal("missing_user_info"))?,
347 email: text("email", "email")
348 .filter(|value| !value.is_empty())
349 .ok_or_else(|| AuthError::internal("missing_user_info"))?,
350 name: text("name", "name"),
351 image: text("image", "picture"),
352 email_verified: self.config.trust_email_verified
353 && super::provider_email_verified(
354 profile.get(field("emailVerified", "email_verified")),
355 ),
356 additional_fields: serde_json::Map::new(),
357 };
358 let tokens = OAuthTokenSet {
359 id_token: raw
360 .get("id_token")
361 .and_then(Value::as_str)
362 .map(str::to_owned),
363 access_token: Some(access.into()),
364 refresh_token: raw
365 .get("refresh_token")
366 .and_then(Value::as_str)
367 .map(str::to_owned),
368 token_type: raw
369 .get("token_type")
370 .and_then(Value::as_str)
371 .map(str::to_owned),
372 access_token_expires_at: raw.get("expires_in").and_then(Value::as_i64).and_then(
373 |seconds| {
374 chrono::Utc::now().checked_add_signed(chrono::Duration::seconds(seconds))
375 },
376 ),
377 scopes: raw.get("scope").and_then(Value::as_str).map_or_else(
378 || {
379 config.get("scopes").and_then(Value::as_array).map_or_else(
380 || {
381 vec![
382 "openid".into(),
383 "email".into(),
384 "profile".into(),
385 "offline_access".into(),
386 ]
387 },
388 |scopes| {
389 scopes
390 .iter()
391 .filter_map(Value::as_str)
392 .map(str::to_owned)
393 .collect()
394 },
395 )
396 },
397 |scope| scope.split(' ').map(str::to_owned).collect(),
398 ),
399 raw: Some(raw.clone()),
400 ..Default::default()
401 };
402 Ok((user, tokens, profile, verified_claims))
403 }
404 .await;
405 let (user, tokens, profile, verified_claims) = match exchange {
406 Ok(value) => value,
407 Err(error) => return fail("invalid_provider", Some(&error.to_string())),
408 };
409 let mut scoped = ctx.clone();
410 let mut scoped_config = (*ctx.config).clone();
411 scoped_config
412 .account
413 .account_linking
414 .trusted_providers
415 .retain(|id| id != provider_id);
416 if provider.domain_verified == Some(true)
417 && user
418 .email
419 .split('@')
420 .nth(1)
421 .is_some_and(|domain| super::domains::matches(domain, &provider.domain))
422 {
423 scoped_config
424 .account
425 .account_linking
426 .trusted_providers
427 .push(provider_id.into());
428 }
429 scoped.config = Arc::new(scoped_config);
430 let policy = OAuthProcessPolicy {
431 user_validation_method: Some("sso-oidc"),
432 override_user_info: config
433 .get("overrideUserInfo")
434 .and_then(Value::as_bool)
435 .unwrap_or(false),
436 callback_url: Some(payload.callback_url.clone()),
437 use_updated_user: true,
438 ..Default::default()
439 };
440 let outcome = if self.config.resolve_user.is_some() {
441 let claims =
442 verified_claims.ok_or_else(|| api(400, "id_token_required_for_user_resolution"))?;
443 let mut provider_user = user.public_profile(false);
444 _ = provider_user.entry("name").or_insert_with(|| json!(""));
445 let input = super::SsoUserResolutionInput {
446 protocol: super::SsoProtocol::Oidc,
447 provider_id: provider.provider_id.clone(),
448 account_key: super::SsoAccountKey {
449 issuer: claims
450 .get("iss")
451 .and_then(Value::as_str)
452 .unwrap_or(&provider.issuer)
453 .into(),
454 account_id: user.id.clone(),
455 },
456 provider_user: Value::Object(provider_user),
457 provider_claims: Some(profile.clone()),
458 verified_id_token_claims: Some(claims),
459 provider_attributes: None,
460 provider_reference: super::sign_in::provider_reference(
461 &provider,
462 self.config.domain_verification,
463 )?,
464 };
465 let request = super::user_resolution::Authentication {
466 identity: crate::oauth::handlers::transactional_identity::Identity {
467 provider: provider_id,
468 user: &user,
469 profile: &profile,
470 policy: &policy,
471 tokens: &tokens,
472 disable_signup: self.config.disable_implicit_sign_up
473 && payload.request_sign_up != Some(true),
474 meta,
475 ctx: &scoped,
476 },
477 input,
478 provider: &provider,
479 };
480 match self.resolved_authentication(request).await {
481 Ok(issued) => Ok(issued),
482 Err(error) => {
483 let code = match &error {
484 AuthError::Api {
485 code: Some(code), ..
486 } => code.as_str(),
487 _ => "SSO_USER_RESOLUTION_FAILED",
488 };
489 let message = match &error {
490 AuthError::Api { message, .. } => message.as_str(),
491 _ => "Unable to resolve the SSO user",
492 };
493 return fail(code, (!message.is_empty()).then_some(message));
494 }
495 }
496 } else {
497 process_oauth_sign_in(
498 OAuthIdentity {
499 provider_name: provider_id,
500 user: &user,
501 profile: &profile,
502 },
503 &policy,
504 &tokens,
505 self.config.disable_implicit_sign_up && payload.request_sign_up != Some(true),
506 meta,
507 &scoped,
508 )
509 .await
510 };
511 let issued = match outcome {
512 Ok(issued) => issued,
513 Err(error) => {
514 let (code, description) =
515 if let crate::oauth::handlers::OAuthSignInError::Generic(message) = &error {
516 (message.clone(), None)
517 } else {
518 error.redirect_parts()
519 };
520 return fail(&code, description);
521 }
522 };
523 self.provision(&issued, &user, Some(&tokens), &provider)
524 .await?;
525 self.provision_organization(
526 &issued.user,
527 &provider,
528 ctx,
529 &Value::Object(user.public_profile(true)),
530 Some(&tokens),
531 )
532 .await?;
533 let destination = if issued.is_register {
534 payload
535 .new_user_url
536 .as_deref()
537 .unwrap_or(&payload.callback_url)
538 } else {
539 &payload.callback_url
540 };
541 let mut response = crate::authentication_helpers::with_session_cookies(
542 crate::authentication_helpers::redirect(destination),
543 &ctx.config,
544 &issued.session.token,
545 false,
546 )?;
547 response.headers.append("Set-Cookie", clear);
548 Ok(EndpointResponse::from_http_response(response))
549 }
550}
551fn redirect_error(
552 target: &str,
553 error: &str,
554 description: Option<&str>,
555) -> AuthResult<EndpointResponse> {
556 let mut url =
557 url::Url::parse(target).map_err(|error| AuthError::internal(error.to_string()))?;
558 {
559 let mut query = url.query_pairs_mut();
560 _ = query.append_pair("error", error);
561 if let Some(description) = description {
562 _ = query.append_pair("error_description", description);
563 }
564 }
565 Ok(EndpointResponse::from_http_response(
566 crate::authentication_helpers::redirect(url.as_str()),
567 ))
568}
569pub(super) async fn outbound_client<S: AuthSchema>(
570 endpoint: &str,
571 ctx: &AuthContext<S>,
572) -> AuthResult<(reqwest::Client, url::Url)> {
573 let mut builder = reqwest::Client::builder()
574 .redirect(reqwest::redirect::Policy::none())
575 .timeout(std::time::Duration::from_secs(10))
576 .no_proxy();
577 let url = url::Url::parse(endpoint).map_err(|e| AuthError::internal(e.to_string()))?;
578 if !matches!(url.scheme(), "http" | "https")
579 || !url.username().is_empty()
580 || url.password().is_some()
581 {
582 return Err(AuthError::internal("Invalid OIDC endpoint URL"));
583 }
584 if !ctx.config.is_redirect_target_trusted(endpoint) {
585 let host = url
586 .host_str()
587 .ok_or_else(|| AuthError::internal("Invalid OIDC endpoint host"))?;
588 let addresses = tokio::net::lookup_host((host, url.port_or_known_default().unwrap_or(443)))
589 .await
590 .map_err(|e| AuthError::internal(e.to_string()))?
591 .collect::<Vec<_>>();
592 if addresses.is_empty()
593 || addresses.iter().any(|address| match address.ip() {
594 std::net::IpAddr::V4(ip) => {
595 ip.is_private()
596 || ip.is_loopback()
597 || ip.is_link_local()
598 || ip.is_unspecified()
599 || ip.is_multicast()
600 || ip.is_broadcast()
601 }
602 std::net::IpAddr::V6(ip) => {
603 ip.is_loopback()
604 || ip.is_unique_local()
605 || ip.is_unicast_link_local()
606 || ip.is_unspecified()
607 || ip.is_multicast()
608 }
609 })
610 {
611 return Err(AuthError::internal(
612 "OIDC endpoint resolves to a private or reserved address",
613 ));
614 }
615 builder = builder.resolve_to_addrs(host, &addresses);
616 }
617 Ok((
618 builder
619 .build()
620 .map_err(|error| AuthError::internal(error.to_string()))?,
621 url,
622 ))
623}