Skip to content

Commit 143224b

Browse files
authored
Don't automatically insert the openid scope on upstream providers (#4517)
2 parents 04a0154 + 470cc26 commit 143224b

File tree

13 files changed

+55
-36
lines changed

13 files changed

+55
-36
lines changed

crates/config/src/sections/upstream_oauth2.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,14 @@ pub struct SignInWithApple {
400400
pub key_id: String,
401401
}
402402

403+
fn default_scope() -> String {
404+
"openid".to_owned()
405+
}
406+
407+
fn is_default_scope(scope: &str) -> bool {
408+
scope == default_scope()
409+
}
410+
403411
/// Configuration for one upstream OAuth 2 provider.
404412
#[skip_serializing_none]
405413
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
@@ -495,6 +503,9 @@ pub struct Provider {
495503
pub id_token_signed_response_alg: JsonWebSignatureAlg,
496504

497505
/// The scopes to request from the provider
506+
///
507+
/// Defaults to `openid`.
508+
#[serde(default = "default_scope", skip_serializing_if = "is_default_scope")]
498509
pub scope: String,
499510

500511
/// How to discover the provider's configuration

crates/data-model/src/upstream_oauth2/session.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ pub struct UpstreamOAuthAuthorizationSession {
250250
pub provider_id: Ulid,
251251
pub state_str: String,
252252
pub code_challenge_verifier: Option<String>,
253-
pub nonce: String,
253+
pub nonce: Option<String>,
254254
pub created_at: DateTime<Utc>,
255255
}
256256

crates/handlers/src/admin/v1/upstream_oauth_links/delete.rs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,14 +108,7 @@ mod tests {
108108
// Pretend it was linked by an authorization session
109109
let session = repo
110110
.upstream_oauth_session()
111-
.add(
112-
&mut rng,
113-
&state.clock,
114-
&provider,
115-
String::new(),
116-
None,
117-
String::new(),
118-
)
111+
.add(&mut rng, &state.clock, &provider, String::new(), None, None)
119112
.await
120113
.unwrap();
121114

crates/handlers/src/upstream_oauth2/callback.rs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -356,10 +356,12 @@ pub(crate) async fn handler(
356356
)
357357
.map_err(mas_oidc_client::error::IdTokenError::from)?;
358358

359-
// Nonce must match.
360-
mas_jose::claims::NONCE
361-
.extract_required_with_options(&mut claims, session.nonce.as_str())
362-
.map_err(mas_oidc_client::error::IdTokenError::from)?;
359+
// Nonce must match if present.
360+
if let Some(nonce) = session.nonce.as_deref() {
361+
mas_jose::claims::NONCE
362+
.extract_required_with_options(&mut claims, nonce)
363+
.map_err(mas_oidc_client::error::IdTokenError::from)?;
364+
}
363365

364366
context = context.with_id_token_claims(claims);
365367
}

crates/handlers/src/upstream_oauth2/link.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,7 +998,7 @@ mod tests {
998998
&provider,
999999
"state".to_owned(),
10001000
None,
1001-
"nonce".to_owned(),
1001+
None,
10021002
)
10031003
.await
10041004
.unwrap();

crates/oidc-client/src/requests/authorization_code.rs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,9 @@ pub struct AuthorizationValidationData {
191191
pub state: String,
192192

193193
/// A string to mitigate replay attacks.
194-
pub nonce: String,
194+
/// Used when the `openid` scope is set (and therefore we are using OpenID
195+
/// Connect).
196+
pub nonce: Option<String>,
195197

196198
/// The URI where the end-user will be redirected after authorization.
197199
pub redirect_uri: Url,
@@ -216,7 +218,7 @@ fn build_authorization_request(
216218
) -> Result<(FullAuthorizationRequest, AuthorizationValidationData), AuthorizationError> {
217219
let AuthorizationRequestData {
218220
client_id,
219-
mut scope,
221+
scope,
220222
redirect_uri,
221223
code_challenge_methods_supported,
222224
display,
@@ -229,9 +231,13 @@ fn build_authorization_request(
229231
response_mode,
230232
} = authorization_data;
231233

234+
let is_openid = scope.contains(&OPENID);
235+
232236
// Generate a random CSRF "state" token and a nonce.
233237
let state = Alphanumeric.sample_string(rng, 16);
234-
let nonce = Alphanumeric.sample_string(rng, 16);
238+
239+
// Generate a random nonce if we're in 'OpenID Connect' mode
240+
let nonce = is_openid.then(|| Alphanumeric.sample_string(rng, 16));
235241

236242
// Use PKCE, whenever possible.
237243
let (pkce, code_challenge_verifier) = if code_challenge_methods_supported
@@ -255,8 +261,6 @@ fn build_authorization_request(
255261
(None, None)
256262
};
257263

258-
scope.insert(OPENID);
259-
260264
let auth_request = FullAuthorizationRequest {
261265
inner: AuthorizationRequest {
262266
response_type: OAuthAuthorizationEndpointResponseType::Code.into(),
@@ -265,7 +269,7 @@ fn build_authorization_request(
265269
scope,
266270
state: Some(state.clone()),
267271
response_mode,
268-
nonce: Some(nonce.clone()),
272+
nonce: nonce.clone(),
269273
display,
270274
prompt,
271275
max_age,
@@ -442,10 +446,12 @@ pub async fn access_token_with_authorization_code(
442446
.extract_optional_with_options(&mut claims, TokenHash::new(signing_alg, &code))
443447
.map_err(IdTokenError::from)?;
444448

445-
// Nonce must match.
446-
claims::NONCE
447-
.extract_required_with_options(&mut claims, validation_data.nonce.as_str())
448-
.map_err(IdTokenError::from)?;
449+
// Nonce must match if we have one.
450+
if let Some(nonce) = validation_data.nonce.as_deref() {
451+
claims::NONCE
452+
.extract_required_with_options(&mut claims, nonce)
453+
.map_err(IdTokenError::from)?;
454+
}
449455

450456
Some(id_token.into_owned())
451457
} else {

crates/oidc-client/tests/it/requests/authorization_code.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ async fn pass_access_token_with_authorization_code() {
186186
let redirect_uri = Url::parse(REDIRECT_URI).unwrap();
187187
let validation_data = AuthorizationValidationData {
188188
state: "some_state".to_owned(),
189-
nonce: NONCE.to_owned(),
189+
nonce: Some(NONCE.to_owned()),
190190
redirect_uri,
191191
code_challenge_verifier: Some(CODE_VERIFIER.to_owned()),
192192
};
@@ -244,7 +244,7 @@ async fn fail_access_token_with_authorization_code_wrong_nonce() {
244244
let redirect_uri = Url::parse(REDIRECT_URI).unwrap();
245245
let validation_data = AuthorizationValidationData {
246246
state: "some_state".to_owned(),
247-
nonce: "wrong_nonce".to_owned(),
247+
nonce: Some("wrong_nonce".to_owned()),
248248
redirect_uri,
249249
code_challenge_verifier: Some(CODE_VERIFIER.to_owned()),
250250
};
@@ -306,7 +306,7 @@ async fn fail_access_token_with_authorization_code_no_id_token() {
306306
let nonce = "some_nonce".to_owned();
307307
let validation_data = AuthorizationValidationData {
308308
state: "some_state".to_owned(),
309-
nonce: nonce.clone(),
309+
nonce: Some(nonce.clone()),
310310
redirect_uri,
311311
code_challenge_verifier: Some(CODE_VERIFIER.to_owned()),
312312
};

crates/storage-pg/.sqlx/query-37a124678323380357fa9d1375fd125fb35476ac3008e5adbd04a761d5edcd42.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Copyright 2025 New Vector Ltd.
2+
--
3+
-- SPDX-License-Identifier: AGPL-3.0-only
4+
-- Please see LICENSE in the repository root for full details.
5+
6+
-- Make the nonce column optional on the upstream oauth sessions
7+
ALTER TABLE "upstream_oauth_authorization_sessions"
8+
ALTER COLUMN "nonce" DROP NOT NULL;

crates/storage-pg/src/upstream_oauth2/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ mod tests {
108108
&provider,
109109
"some-state".to_owned(),
110110
None,
111-
"some-nonce".to_owned(),
111+
Some("some-nonce".to_owned()),
112112
)
113113
.await
114114
.unwrap();

0 commit comments

Comments
 (0)