Browse Source

SSO: Verify that the userinfo subject matches the id_token subject

OpenID Connect Core 5.3.2 requires that the sub claim of the userinfo
response is verified to exactly match the sub claim of the id_token, and
that the userinfo claims are not used otherwise. The openidconnect crate
implements this check, but only when an expected subject is provided,
and the user_info call passed None, so the check never ran.

Thread the id_token subject through to the userinfo request made during
the initial code exchange, so that a mismatched response is rejected
before its email/email_verified/preferred_username claims are used. The
check_validity probe still passes None since no id_token is available at
that point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull/7463/head
Fredrik Ekre 3 weeks ago
parent
commit
26589d20fc
No known key found for this signature in database GPG Key ID: DE82E6D5E364C0A2
  1. 3
      src/sso.rs
  2. 13
      src/sso_client.rs

3
src/sso.rs

@ -279,7 +279,8 @@ pub async fn exchange_code(
let client = Client::cached().await?; let client = Client::cached().await?;
let (token_response, id_claims) = client.exchange_code(code, client_verifier, &sso_auth).await?; let (token_response, id_claims) = client.exchange_code(code, client_verifier, &sso_auth).await?;
let user_info = client.user_info(token_response.access_token().to_owned()).await?; let user_info =
client.user_info(token_response.access_token().to_owned(), Some(id_claims.subject().clone())).await?;
let email = match id_claims.email().or(user_info.email()) { let email = match id_claims.email().or(user_info.email()) {
None => err!("Neither id token nor userinfo contained an email"), None => err!("Neither id token nor userinfo contained an email"),

13
src/sso_client.rs

@ -5,7 +5,7 @@ use openidconnect::{
AuthorizationRequest, ClientId, ClientSecret, CsrfToken, EmptyAdditionalClaims, EmptyExtraTokenFields, AuthorizationRequest, ClientId, ClientSecret, CsrfToken, EmptyAdditionalClaims, EmptyExtraTokenFields,
EndpointNotSet, EndpointSet, HttpClientError, HttpRequest, HttpResponse, IdTokenClaims, IdTokenFields, Nonce, EndpointNotSet, EndpointSet, HttpClientError, HttpRequest, HttpResponse, IdTokenClaims, IdTokenFields, Nonce,
OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RefreshToken, ResponseType, Scope, StandardErrorResponse, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RefreshToken, ResponseType, Scope, StandardErrorResponse,
StandardTokenResponse, StandardTokenResponse, SubjectIdentifier,
core::{ core::{
CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreClientAuthMethod, CoreErrorResponseType, CoreGenderClaim, CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreClientAuthMethod, CoreErrorResponseType, CoreGenderClaim,
CoreIdTokenVerifier, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, CoreIdTokenVerifier, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm,
@ -272,8 +272,12 @@ impl Client {
} }
} }
pub async fn user_info(&self, access_token: AccessToken) -> ApiResult<CoreUserInfoClaims> { pub async fn user_info(
match self.core_client.user_info(access_token, None).request_async(&self.http_client).await { &self,
access_token: AccessToken,
expected_subject: Option<SubjectIdentifier>,
) -> ApiResult<CoreUserInfoClaims> {
match self.core_client.user_info(access_token, expected_subject).request_async(&self.http_client).await {
Err(err) => err!(format!("Request to user_info endpoint failed: {err}")), Err(err) => err!(format!("Request to user_info endpoint failed: {err}")),
Ok(user_info) => Ok(user_info), Ok(user_info) => Ok(user_info),
} }
@ -281,7 +285,8 @@ impl Client {
pub async fn check_validity(access_token: String) -> EmptyResult { pub async fn check_validity(access_token: String) -> EmptyResult {
let client = Client::cached().await?; let client = Client::cached().await?;
match client.user_info(AccessToken::new(access_token)).await { // No expected subject to verify against since only the access_token is kept after login
match client.user_info(AccessToken::new(access_token), None).await {
Err(err) => { Err(err) => {
err_silent!(format!("Failed to retrieve user info, token has probably been invalidated: {err}")) err_silent!(format!("Failed to retrieve user info, token has probably been invalidated: {err}"))
} }

Loading…
Cancel
Save