Skip to content

Commit 80903ed

Browse files
authored
Add id_token_signed_response_alg and userinfo_signed_response_alg (#3664)
1 parent 0fafef3 commit 80903ed

22 files changed

+338
-131
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cli/src/sync.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,9 +280,8 @@ pub async fn config_sync(
280280
brand_name: provider.brand_name,
281281
scope: provider.scope.parse()?,
282282
token_endpoint_auth_method,
283-
token_endpoint_signing_alg: provider
284-
.token_endpoint_auth_signing_alg
285-
.clone(),
283+
token_endpoint_signing_alg: provider.token_endpoint_auth_signing_alg,
284+
id_token_signed_response_alg: provider.id_token_signed_response_alg,
286285
client_id: provider.client_id,
287286
encrypted_client_secret,
288287
claims_imports: map_claims_imports(&provider.claims_imports),
@@ -293,6 +292,7 @@ pub async fn config_sync(
293292
discovery_mode,
294293
pkce_mode,
295294
fetch_userinfo: provider.fetch_userinfo,
295+
userinfo_signed_response_alg: provider.userinfo_signed_response_alg,
296296
response_mode,
297297
additional_authorization_parameters: provider
298298
.additional_authorization_parameters

crates/config/src/sections/upstream_oauth2.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,16 @@ fn is_default_true(value: &bool) -> bool {
398398
*value
399399
}
400400

401+
#[allow(clippy::ref_option)]
402+
fn is_signed_response_alg_default(signed_response_alg: &JsonWebSignatureAlg) -> bool {
403+
*signed_response_alg == signed_response_alg_default()
404+
}
405+
406+
#[allow(clippy::unnecessary_wraps)]
407+
fn signed_response_alg_default() -> JsonWebSignatureAlg {
408+
JsonWebSignatureAlg::Rs256
409+
}
410+
401411
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
402412
pub struct SignInWithApple {
403413
/// The private key used to sign the `id_token`
@@ -472,6 +482,16 @@ pub struct Provider {
472482
#[serde(skip_serializing_if = "Option::is_none")]
473483
pub token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
474484

485+
/// Expected signature for the JWT payload returned by the token
486+
/// authentication endpoint.
487+
///
488+
/// Defaults to `RS256`.
489+
#[serde(
490+
default = "signed_response_alg_default",
491+
skip_serializing_if = "is_signed_response_alg_default"
492+
)]
493+
pub id_token_signed_response_alg: JsonWebSignatureAlg,
494+
475495
/// The scopes to request from the provider
476496
pub scope: String,
477497

@@ -497,6 +517,14 @@ pub struct Provider {
497517
#[serde(default)]
498518
pub fetch_userinfo: bool,
499519

520+
/// Expected signature for the JWT payload returned by the userinfo
521+
/// endpoint.
522+
///
523+
/// If not specified, the response is expected to be an unsigned JSON
524+
/// payload.
525+
#[serde(skip_serializing_if = "Option::is_none")]
526+
pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
527+
500528
/// The URL to use for the provider's authorization endpoint
501529
///
502530
/// Defaults to the `authorization_endpoint` provided through discovery

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,10 +230,12 @@ pub struct UpstreamOAuthProvider {
230230
pub token_endpoint_override: Option<Url>,
231231
pub userinfo_endpoint_override: Option<Url>,
232232
pub fetch_userinfo: bool,
233+
pub userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
233234
pub client_id: String,
234235
pub encrypted_client_secret: Option<String>,
235236
pub token_endpoint_signing_alg: Option<JsonWebSignatureAlg>,
236237
pub token_endpoint_auth_method: TokenAuthMethod,
238+
pub id_token_signed_response_alg: JsonWebSignatureAlg,
237239
pub response_mode: ResponseMode,
238240
pub created_at: DateTime<Utc>,
239241
pub disabled_at: Option<DateTime<Utc>>,

crates/handlers/src/upstream_oauth2/cache.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ mod tests {
289289
use mas_data_model::{
290290
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod,
291291
};
292+
use mas_iana::jose::JsonWebSignatureAlg;
292293
use mas_storage::{clock::MockClock, Clock};
293294
use oauth2_types::scope::{Scope, OPENID};
294295
use ulid::Ulid;
@@ -400,6 +401,7 @@ mod tests {
400401
discovery_mode: UpstreamOAuthProviderDiscoveryMode::Insecure,
401402
pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
402403
fetch_userinfo: false,
404+
userinfo_signed_response_alg: None,
403405
jwks_uri_override: None,
404406
authorization_endpoint_override: None,
405407
scope: Scope::from_iter([OPENID]),
@@ -410,6 +412,7 @@ mod tests {
410412
token_endpoint_signing_alg: None,
411413
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
412414
response_mode: mas_data_model::UpstreamOAuthProviderResponseMode::Query,
415+
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
413416
created_at: clock.now(),
414417
disabled_at: None,
415418
claims_imports: UpstreamOAuthProviderClaimsImports::default(),

crates/handlers/src/upstream_oauth2/callback.rs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -274,25 +274,26 @@ pub(crate) async fn handler(
274274
)
275275
.await?;
276276

277+
let mut jwks = None;
278+
277279
let mut context = AttributeMappingContext::new();
278280
if let Some(id_token) = token_response.id_token.as_ref() {
279-
// Fetch the JWKS
280-
let jwks =
281+
jwks = Some(
281282
mas_oidc_client::requests::jose::fetch_jwks(&client, lazy_metadata.jwks_uri().await?)
282-
.await?;
283+
.await?,
284+
);
283285

284-
let verification_data = JwtVerificationData {
286+
let id_token_verification_data = JwtVerificationData {
285287
issuer: &provider.issuer,
286-
jwks: &jwks,
287-
// TODO: make that configurable
288-
signing_algorithm: &mas_iana::jose::JsonWebSignatureAlg::Rs256,
288+
jwks: jwks.as_ref().unwrap(),
289+
signing_algorithm: &provider.id_token_signed_response_alg,
289290
client_id: &provider.client_id,
290291
};
291292

292293
// Decode and verify the ID token
293294
let id_token = mas_oidc_client::requests::jose::verify_id_token(
294295
id_token,
295-
verification_data,
296+
id_token_verification_data,
296297
None,
297298
clock.now(),
298299
)?;
@@ -304,7 +305,7 @@ pub(crate) async fn handler(
304305
.extract_optional_with_options(
305306
&mut claims,
306307
TokenHash::new(
307-
verification_data.signing_algorithm,
308+
id_token_verification_data.signing_algorithm,
308309
&token_response.access_token,
309310
),
310311
)
@@ -314,7 +315,7 @@ pub(crate) async fn handler(
314315
mas_jose::claims::C_HASH
315316
.extract_optional_with_options(
316317
&mut claims,
317-
TokenHash::new(verification_data.signing_algorithm, &code),
318+
TokenHash::new(id_token_verification_data.signing_algorithm, &code),
318319
)
319320
.map_err(mas_oidc_client::error::IdTokenError::from)?;
320321

@@ -331,15 +332,42 @@ pub(crate) async fn handler(
331332
}
332333

333334
let userinfo = if provider.fetch_userinfo {
334-
Some(json!(
335-
mas_oidc_client::requests::userinfo::fetch_userinfo(
336-
&client,
337-
lazy_metadata.userinfo_endpoint().await?,
338-
token_response.access_token.as_str(),
339-
None,
340-
)
341-
.await?
342-
))
335+
Some(json!(match &provider.userinfo_signed_response_alg {
336+
Some(signing_algorithm) => {
337+
let jwks = match jwks {
338+
Some(jwks) => jwks,
339+
None => {
340+
mas_oidc_client::requests::jose::fetch_jwks(
341+
&client,
342+
lazy_metadata.jwks_uri().await?,
343+
)
344+
.await?
345+
}
346+
};
347+
348+
mas_oidc_client::requests::userinfo::fetch_userinfo(
349+
&client,
350+
lazy_metadata.userinfo_endpoint().await?,
351+
token_response.access_token.as_str(),
352+
Some(JwtVerificationData {
353+
issuer: &provider.issuer,
354+
jwks: &jwks,
355+
signing_algorithm,
356+
client_id: &provider.client_id,
357+
}),
358+
)
359+
.await?
360+
}
361+
None => {
362+
mas_oidc_client::requests::userinfo::fetch_userinfo(
363+
&client,
364+
lazy_metadata.userinfo_endpoint().await?,
365+
token_response.access_token.as_str(),
366+
None,
367+
)
368+
.await?
369+
}
370+
}))
343371
} else {
344372
None
345373
};

crates/handlers/src/upstream_oauth2/link.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,13 +922,15 @@ mod tests {
922922
scope: Scope::from_iter([OPENID]),
923923
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
924924
token_endpoint_signing_alg: None,
925+
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
925926
client_id: "client".to_owned(),
926927
encrypted_client_secret: None,
927928
claims_imports,
928929
authorization_endpoint_override: None,
929930
token_endpoint_override: None,
930931
userinfo_endpoint_override: None,
931932
fetch_userinfo: false,
933+
userinfo_signed_response_alg: None,
932934
jwks_uri_override: None,
933935
discovery_mode: mas_data_model::UpstreamOAuthProviderDiscoveryMode::Oidc,
934936
pkce_mode: mas_data_model::UpstreamOAuthProviderPkceMode::Auto,

crates/handlers/src/views/login.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ mod test {
346346
use mas_data_model::{
347347
UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderTokenAuthMethod,
348348
};
349+
use mas_iana::jose::JsonWebSignatureAlg;
349350
use mas_router::Route;
350351
use mas_storage::{
351352
upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository},
@@ -403,7 +404,9 @@ mod test {
403404
scope: [OPENID].into_iter().collect(),
404405
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
405406
token_endpoint_signing_alg: None,
407+
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
406408
fetch_userinfo: false,
409+
userinfo_signed_response_alg: None,
407410
client_id: "client".to_owned(),
408411
encrypted_client_secret: None,
409412
claims_imports: UpstreamOAuthProviderClaimsImports::default(),
@@ -441,7 +444,9 @@ mod test {
441444
scope: [OPENID].into_iter().collect(),
442445
token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::None,
443446
token_endpoint_signing_alg: None,
447+
id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
444448
fetch_userinfo: false,
449+
userinfo_signed_response_alg: None,
445450
client_id: "client".to_owned(),
446451
encrypted_client_secret: None,
447452
claims_imports: UpstreamOAuthProviderClaimsImports::default(),
Lines changed: 25 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)