Skip to main content

alibi_plugins/sso/
callback.rs

1//! OIDC callback state admission, grant redemption, and session issuance.
2use 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        // Shared callbacks derive the provider only from protected state.
103        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}