From 26589d20fc3b4f7f5308bb9f6829cf52dcf9849c Mon Sep 17 00:00:00 2001 From: Fredrik Ekre Date: Thu, 23 Jul 2026 22:20:38 +0200 Subject: [PATCH] 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 --- src/sso.rs | 3 ++- src/sso_client.rs | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/sso.rs b/src/sso.rs index 01fbd906..08d0a88b 100644 --- a/src/sso.rs +++ b/src/sso.rs @@ -279,7 +279,8 @@ pub async fn exchange_code( let client = Client::cached().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()) { None => err!("Neither id token nor userinfo contained an email"), diff --git a/src/sso_client.rs b/src/sso_client.rs index ff39b0b0..5c3ee73d 100644 --- a/src/sso_client.rs +++ b/src/sso_client.rs @@ -5,7 +5,7 @@ use openidconnect::{ AuthorizationRequest, ClientId, ClientSecret, CsrfToken, EmptyAdditionalClaims, EmptyExtraTokenFields, EndpointNotSet, EndpointSet, HttpClientError, HttpRequest, HttpResponse, IdTokenClaims, IdTokenFields, Nonce, OAuth2TokenResponse, PkceCodeChallenge, PkceCodeVerifier, RefreshToken, ResponseType, Scope, StandardErrorResponse, - StandardTokenResponse, + StandardTokenResponse, SubjectIdentifier, core::{ CoreAuthDisplay, CoreAuthPrompt, CoreClient, CoreClientAuthMethod, CoreErrorResponseType, CoreGenderClaim, CoreIdTokenVerifier, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm, CoreJwsSigningAlgorithm, @@ -272,8 +272,12 @@ impl Client { } } - pub async fn user_info(&self, access_token: AccessToken) -> ApiResult { - match self.core_client.user_info(access_token, None).request_async(&self.http_client).await { + pub async fn user_info( + &self, + access_token: AccessToken, + expected_subject: Option, + ) -> ApiResult { + 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}")), Ok(user_info) => Ok(user_info), } @@ -281,7 +285,8 @@ impl Client { pub async fn check_validity(access_token: String) -> EmptyResult { 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_silent!(format!("Failed to retrieve user info, token has probably been invalidated: {err}")) }