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: match profile.get(field("email", "email")) {
349 Some(Value::Array(values)) => values
350 .iter()
351 .filter_map(Value::as_str)
352 .find(|value| !value.is_empty()),
353 value => value.and_then(Value::as_str),
354 }
355 .filter(|value| !value.is_empty())
356 .map(str::to_owned)
357 .ok_or_else(|| AuthError::internal("missing_user_info"))?,
358 name: text("name", "name"),
359 image: text("image", "picture"),
360 email_verified: self.config.trust_email_verified
361 && super::provider_email_verified(
362 profile.get(field("emailVerified", "email_verified")),
363 ),
364 additional_fields: serde_json::Map::new(),
365 };
366 let tokens = OAuthTokenSet {
367 id_token: raw
368 .get("id_token")
369 .and_then(Value::as_str)
370 .map(str::to_owned),
371 access_token: Some(access.into()),
372 refresh_token: raw
373 .get("refresh_token")
374 .and_then(Value::as_str)
375 .map(str::to_owned),
376 token_type: raw
377 .get("token_type")
378 .and_then(Value::as_str)
379 .map(str::to_owned),
380 access_token_expires_at: raw.get("expires_in").and_then(Value::as_i64).and_then(
381 |seconds| {
382 chrono::Utc::now().checked_add_signed(chrono::Duration::seconds(seconds))
383 },
384 ),
385 scopes: raw.get("scope").and_then(Value::as_str).map_or_else(
386 || {
387 config.get("scopes").and_then(Value::as_array).map_or_else(
388 || {
389 vec![
390 "openid".into(),
391 "email".into(),
392 "profile".into(),
393 "offline_access".into(),
394 ]
395 },
396 |scopes| {
397 scopes
398 .iter()
399 .filter_map(Value::as_str)
400 .map(str::to_owned)
401 .collect()
402 },
403 )
404 },
405 |scope| scope.split(' ').map(str::to_owned).collect(),
406 ),
407 raw: Some(raw.clone()),
408 ..Default::default()
409 };
410 Ok((user, tokens, profile, verified_claims))
411 }
412 .await;
413 let (user, tokens, profile, verified_claims) = match exchange {
414 Ok(value) => value,
415 Err(error) => return fail("invalid_provider", Some(&error.to_string())),
416 };
417 let mut scoped = ctx.clone();
418 let mut scoped_config = (*ctx.config).clone();
419 scoped_config
420 .account
421 .account_linking
422 .trusted_providers
423 .retain(|id| id != provider_id);
424 if provider.domain_verified == Some(true)
425 && user
426 .email
427 .split('@')
428 .nth(1)
429 .is_some_and(|domain| super::domains::matches(domain, &provider.domain))
430 {
431 scoped_config
432 .account
433 .account_linking
434 .trusted_providers
435 .push(provider_id.into());
436 }
437 scoped.config = Arc::new(scoped_config);
438 let policy = OAuthProcessPolicy {
439 user_validation_method: Some("sso-oidc"),
440 override_user_info: config
441 .get("overrideUserInfo")
442 .and_then(Value::as_bool)
443 .unwrap_or(false),
444 callback_url: Some(payload.callback_url.clone()),
445 use_updated_user: true,
446 ..Default::default()
447 };
448 let outcome = if self.config.resolve_user.is_some() {
449 let claims =
450 verified_claims.ok_or_else(|| api(400, "id_token_required_for_user_resolution"))?;
451 let mut provider_user = user.public_profile(false);
452 _ = provider_user.entry("name").or_insert_with(|| json!(""));
453 let input = super::SsoUserResolutionInput {
454 protocol: super::SsoProtocol::Oidc,
455 provider_id: provider.provider_id.clone(),
456 account_key: super::SsoAccountKey {
457 issuer: claims
458 .get("iss")
459 .and_then(Value::as_str)
460 .unwrap_or(&provider.issuer)
461 .into(),
462 account_id: user.id.clone(),
463 },
464 provider_user: Value::Object(provider_user),
465 provider_claims: Some(profile.clone()),
466 verified_id_token_claims: Some(claims),
467 provider_attributes: None,
468 provider_reference: super::sign_in::provider_reference(
469 &provider,
470 self.config.domain_verification,
471 )?,
472 };
473 let request = super::user_resolution::Authentication {
474 identity: crate::oauth::handlers::transactional_identity::Identity {
475 provider: provider_id,
476 user: &user,
477 profile: &profile,
478 policy: &policy,
479 tokens: &tokens,
480 disable_signup: self.config.disable_implicit_sign_up
481 && payload.request_sign_up != Some(true),
482 meta,
483 ctx: &scoped,
484 },
485 input,
486 provider: &provider,
487 };
488 match self.resolved_authentication(request).await {
489 Ok(issued) => Ok(issued),
490 Err(error) => {
491 let code = match &error {
492 AuthError::Api {
493 code: Some(code), ..
494 } => code.as_str(),
495 _ => "SSO_USER_RESOLUTION_FAILED",
496 };
497 let message = match &error {
498 AuthError::Api { message, .. } => message.as_str(),
499 _ => "Unable to resolve the SSO user",
500 };
501 return fail(code, (!message.is_empty()).then_some(message));
502 }
503 }
504 } else {
505 process_oauth_sign_in(
506 OAuthIdentity {
507 provider_name: provider_id,
508 user: &user,
509 profile: &profile,
510 },
511 &policy,
512 &tokens,
513 self.config.disable_implicit_sign_up && payload.request_sign_up != Some(true),
514 meta,
515 &scoped,
516 )
517 .await
518 };
519 let issued = match outcome {
520 Ok(issued) => issued,
521 Err(error) => {
522 let (code, description) =
523 if let crate::oauth::handlers::OAuthSignInError::Generic(message) = &error {
524 (message.clone(), None)
525 } else {
526 error.redirect_parts()
527 };
528 return fail(&code, description);
529 }
530 };
531 self.provision(&issued, &user, Some(&tokens), &provider)
532 .await?;
533 self.provision_organization(
534 &issued.user,
535 &provider,
536 ctx,
537 &Value::Object(user.public_profile(true)),
538 Some(&tokens),
539 )
540 .await?;
541 let destination = if issued.is_register {
542 payload
543 .new_user_url
544 .as_deref()
545 .unwrap_or(&payload.callback_url)
546 } else {
547 &payload.callback_url
548 };
549 let mut response = crate::authentication_helpers::with_session_cookies(
550 crate::authentication_helpers::redirect(destination),
551 &ctx.config,
552 &issued.session.token,
553 false,
554 )?;
555 response.headers.append("Set-Cookie", clear);
556 Ok(EndpointResponse::from_http_response(response))
557 }
558}
559fn redirect_error(
560 target: &str,
561 error: &str,
562 description: Option<&str>,
563) -> AuthResult<EndpointResponse> {
564 let mut url =
565 url::Url::parse(target).map_err(|error| AuthError::internal(error.to_string()))?;
566 {
567 let mut query = url.query_pairs_mut();
568 _ = query.append_pair("error", error);
569 if let Some(description) = description {
570 _ = query.append_pair("error_description", description);
571 }
572 }
573 Ok(EndpointResponse::from_http_response(
574 crate::authentication_helpers::redirect(url.as_str()),
575 ))
576}
577pub(super) async fn outbound_client<S: AuthSchema>(
578 endpoint: &str,
579 ctx: &AuthContext<S>,
580) -> AuthResult<(reqwest::Client, url::Url)> {
581 let mut builder = reqwest::Client::builder()
582 .redirect(reqwest::redirect::Policy::none())
583 .timeout(std::time::Duration::from_secs(10))
584 .no_proxy();
585 let url = url::Url::parse(endpoint).map_err(|e| AuthError::internal(e.to_string()))?;
586 if !matches!(url.scheme(), "http" | "https")
587 || !url.username().is_empty()
588 || url.password().is_some()
589 {
590 return Err(AuthError::internal("Invalid OIDC endpoint URL"));
591 }
592 if !ctx.config.is_redirect_target_trusted(endpoint) {
593 let host = url
594 .host_str()
595 .ok_or_else(|| AuthError::internal("Invalid OIDC endpoint host"))?;
596 let addresses = tokio::net::lookup_host((host, url.port_or_known_default().unwrap_or(443)))
597 .await
598 .map_err(|e| AuthError::internal(e.to_string()))?
599 .collect::<Vec<_>>();
600 if addresses.is_empty()
601 || addresses.iter().any(|address| match address.ip() {
602 std::net::IpAddr::V4(ip) => {
603 ip.is_private()
604 || ip.is_loopback()
605 || ip.is_link_local()
606 || ip.is_unspecified()
607 || ip.is_multicast()
608 || ip.is_broadcast()
609 }
610 std::net::IpAddr::V6(ip) => {
611 ip.is_loopback()
612 || ip.is_unique_local()
613 || ip.is_unicast_link_local()
614 || ip.is_unspecified()
615 || ip.is_multicast()
616 }
617 })
618 {
619 return Err(AuthError::internal(
620 "OIDC endpoint resolves to a private or reserved address",
621 ));
622 }
623 builder = builder.resolve_to_addrs(host, &addresses);
624 }
625 Ok((
626 builder
627 .build()
628 .map_err(|error| AuthError::internal(error.to_string()))?,
629 url,
630 ))
631}