Don't generate and send a nonce for non-OIDC-compliant auth requests
This commit is contained in:
@@ -250,7 +250,7 @@ pub struct UpstreamOAuthAuthorizationSession {
|
||||
pub provider_id: Ulid,
|
||||
pub state_str: String,
|
||||
pub code_challenge_verifier: Option<String>,
|
||||
pub nonce: String,
|
||||
pub nonce: Option<String>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
||||
@@ -108,14 +108,7 @@ mod tests {
|
||||
// Pretend it was linked by an authorization session
|
||||
let session = repo
|
||||
.upstream_oauth_session()
|
||||
.add(
|
||||
&mut rng,
|
||||
&state.clock,
|
||||
&provider,
|
||||
String::new(),
|
||||
None,
|
||||
String::new(),
|
||||
)
|
||||
.add(&mut rng, &state.clock, &provider, String::new(), None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -356,10 +356,12 @@ pub(crate) async fn handler(
|
||||
)
|
||||
.map_err(mas_oidc_client::error::IdTokenError::from)?;
|
||||
|
||||
// Nonce must match.
|
||||
mas_jose::claims::NONCE
|
||||
.extract_required_with_options(&mut claims, session.nonce.as_str())
|
||||
.map_err(mas_oidc_client::error::IdTokenError::from)?;
|
||||
// Nonce must match if present.
|
||||
if let Some(nonce) = session.nonce.as_deref() {
|
||||
mas_jose::claims::NONCE
|
||||
.extract_required_with_options(&mut claims, nonce)
|
||||
.map_err(mas_oidc_client::error::IdTokenError::from)?;
|
||||
}
|
||||
|
||||
context = context.with_id_token_claims(claims);
|
||||
}
|
||||
|
||||
@@ -998,7 +998,7 @@ mod tests {
|
||||
&provider,
|
||||
"state".to_owned(),
|
||||
None,
|
||||
"nonce".to_owned(),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -191,7 +191,7 @@ pub struct AuthorizationValidationData {
|
||||
pub state: String,
|
||||
|
||||
/// A string to mitigate replay attacks.
|
||||
pub nonce: String,
|
||||
pub nonce: Option<String>,
|
||||
|
||||
/// The URI where the end-user will be redirected after authorization.
|
||||
pub redirect_uri: Url,
|
||||
@@ -216,7 +216,7 @@ fn build_authorization_request(
|
||||
) -> Result<(FullAuthorizationRequest, AuthorizationValidationData), AuthorizationError> {
|
||||
let AuthorizationRequestData {
|
||||
client_id,
|
||||
mut scope,
|
||||
scope,
|
||||
redirect_uri,
|
||||
code_challenge_methods_supported,
|
||||
display,
|
||||
@@ -229,9 +229,13 @@ fn build_authorization_request(
|
||||
response_mode,
|
||||
} = authorization_data;
|
||||
|
||||
let is_openid = scope.contains(&OPENID);
|
||||
|
||||
// Generate a random CSRF "state" token and a nonce.
|
||||
let state = Alphanumeric.sample_string(rng, 16);
|
||||
let nonce = Alphanumeric.sample_string(rng, 16);
|
||||
|
||||
// Generate a random nonce if we're in 'OpenID Connect' mode
|
||||
let nonce = is_openid.then(|| Alphanumeric.sample_string(rng, 16));
|
||||
|
||||
// Use PKCE, whenever possible.
|
||||
let (pkce, code_challenge_verifier) = if code_challenge_methods_supported
|
||||
@@ -263,7 +267,7 @@ fn build_authorization_request(
|
||||
scope,
|
||||
state: Some(state.clone()),
|
||||
response_mode,
|
||||
nonce: Some(nonce.clone()),
|
||||
nonce: nonce.clone(),
|
||||
display,
|
||||
prompt,
|
||||
max_age,
|
||||
@@ -440,10 +444,12 @@ pub async fn access_token_with_authorization_code(
|
||||
.extract_optional_with_options(&mut claims, TokenHash::new(signing_alg, &code))
|
||||
.map_err(IdTokenError::from)?;
|
||||
|
||||
// Nonce must match.
|
||||
claims::NONCE
|
||||
.extract_required_with_options(&mut claims, validation_data.nonce.as_str())
|
||||
.map_err(IdTokenError::from)?;
|
||||
// Nonce must match if we have one.
|
||||
if let Some(nonce) = validation_data.nonce.as_deref() {
|
||||
claims::NONCE
|
||||
.extract_required_with_options(&mut claims, nonce)
|
||||
.map_err(IdTokenError::from)?;
|
||||
}
|
||||
|
||||
Some(id_token.into_owned())
|
||||
} else {
|
||||
|
||||
@@ -186,7 +186,7 @@ async fn pass_access_token_with_authorization_code() {
|
||||
let redirect_uri = Url::parse(REDIRECT_URI).unwrap();
|
||||
let validation_data = AuthorizationValidationData {
|
||||
state: "some_state".to_owned(),
|
||||
nonce: NONCE.to_owned(),
|
||||
nonce: Some(NONCE.to_owned()),
|
||||
redirect_uri,
|
||||
code_challenge_verifier: Some(CODE_VERIFIER.to_owned()),
|
||||
};
|
||||
@@ -244,7 +244,7 @@ async fn fail_access_token_with_authorization_code_wrong_nonce() {
|
||||
let redirect_uri = Url::parse(REDIRECT_URI).unwrap();
|
||||
let validation_data = AuthorizationValidationData {
|
||||
state: "some_state".to_owned(),
|
||||
nonce: "wrong_nonce".to_owned(),
|
||||
nonce: Some("wrong_nonce".to_owned()),
|
||||
redirect_uri,
|
||||
code_challenge_verifier: Some(CODE_VERIFIER.to_owned()),
|
||||
};
|
||||
@@ -306,7 +306,7 @@ async fn fail_access_token_with_authorization_code_no_id_token() {
|
||||
let nonce = "some_nonce".to_owned();
|
||||
let validation_data = AuthorizationValidationData {
|
||||
state: "some_state".to_owned(),
|
||||
nonce: nonce.clone(),
|
||||
nonce: Some(nonce.clone()),
|
||||
redirect_uri,
|
||||
code_challenge_verifier: Some(CODE_VERIFIER.to_owned()),
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- Make the nonce column optional on the upstream oauth sessions
|
||||
ALTER TABLE "upstream_oauth_authorization_sessions"
|
||||
ALTER COLUMN "nonce" DROP NOT NULL;
|
||||
@@ -108,7 +108,7 @@ mod tests {
|
||||
&provider,
|
||||
"some-state".to_owned(),
|
||||
None,
|
||||
"some-nonce".to_owned(),
|
||||
Some("some-nonce".to_owned()),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -38,7 +38,7 @@ struct SessionLookup {
|
||||
upstream_oauth_link_id: Option<Uuid>,
|
||||
state: String,
|
||||
code_challenge_verifier: Option<String>,
|
||||
nonce: String,
|
||||
nonce: Option<String>,
|
||||
id_token: Option<String>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
created_at: DateTime<Utc>,
|
||||
@@ -191,7 +191,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> {
|
||||
upstream_oauth_provider: &UpstreamOAuthProvider,
|
||||
state_str: String,
|
||||
code_challenge_verifier: Option<String>,
|
||||
nonce: String,
|
||||
nonce: Option<String>,
|
||||
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error> {
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_source(created_at.into(), rng);
|
||||
|
||||
@@ -48,7 +48,7 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
|
||||
/// upstream OAuth provider
|
||||
/// * `code_challenge_verifier`: the code challenge verifier used in this
|
||||
/// session, if PKCE is being used
|
||||
/// * `nonce`: the `nonce` used in this session
|
||||
/// * `nonce`: the `nonce` used in this session if in OIDC mode
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -60,7 +60,7 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
|
||||
upstream_oauth_provider: &UpstreamOAuthProvider,
|
||||
state: String,
|
||||
code_challenge_verifier: Option<String>,
|
||||
nonce: String,
|
||||
nonce: Option<String>,
|
||||
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
|
||||
|
||||
/// Mark a session as completed and associate the given link
|
||||
@@ -122,7 +122,7 @@ repository_impl!(UpstreamOAuthSessionRepository:
|
||||
upstream_oauth_provider: &UpstreamOAuthProvider,
|
||||
state: String,
|
||||
code_challenge_verifier: Option<String>,
|
||||
nonce: String,
|
||||
nonce: Option<String>,
|
||||
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
|
||||
|
||||
async fn complete_with_link(
|
||||
|
||||
Reference in New Issue
Block a user