Skip to content

Commit 3f66940

Browse files
committed
Sort client registration metadata fields before hashing
1 parent 4294dc8 commit 3f66940

File tree

4 files changed

+81
-3
lines changed

4 files changed

+81
-3
lines changed

crates/handlers/src/oauth2/registration.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ pub(crate) async fn post(
206206
// Propagate any JSON extraction error
207207
let Json(body) = body?;
208208

209+
// Sort the properties to ensure a stable serialisation order for hashing
210+
let body = body.sorted();
211+
209212
// We need to serialize the body to compute the hash, and to log it
210213
let body_json = serde_json::to_string(&body)?;
211214

@@ -529,9 +532,13 @@ mod tests {
529532
let request =
530533
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
531534
"client_uri": "https://example.com/",
532-
"redirect_uris": ["https://example.com/"],
535+
"client_name": "Example",
536+
"client_name#en": "Example",
537+
"client_name#fr": "Exemple",
538+
"client_name#de": "Beispiel",
539+
"redirect_uris": ["https://example.com/", "https://example.com/callback"],
533540
"response_types": ["code"],
534-
"grant_types": ["authorization_code"],
541+
"grant_types": ["authorization_code", "urn:ietf:params:oauth:grant-type:device_code"],
535542
"token_endpoint_auth_method": "none",
536543
}));
537544

@@ -545,6 +552,25 @@ mod tests {
545552
let response: ClientRegistrationResponse = response.json();
546553
assert_eq!(response.client_id, client_id);
547554

555+
// Check that the order of some properties doesn't matter
556+
let request =
557+
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({
558+
"client_uri": "https://example.com/",
559+
"client_name": "Example",
560+
"client_name#de": "Beispiel",
561+
"client_name#fr": "Exemple",
562+
"client_name#en": "Example",
563+
"redirect_uris": ["https://example.com/callback", "https://example.com/"],
564+
"response_types": ["code"],
565+
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "authorization_code"],
566+
"token_endpoint_auth_method": "none",
567+
}));
568+
569+
let response = state.request(request).await;
570+
response.assert_status(StatusCode::CREATED);
571+
let response: ClientRegistrationResponse = response.json();
572+
assert_eq!(response.client_id, client_id);
573+
548574
// Doing that with a client that has a client_secret should not deduplicate
549575
let request =
550576
Request::post(mas_router::OAuth2RegistrationEndpoint::PATH).json(serde_json::json!({

crates/oauth2-types/src/registration/client_metadata_serde.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ impl<T> Localized<T> {
8080
localized,
8181
}))
8282
}
83+
84+
/// Sort the localized keys. This is inteded to ensure a stable
85+
/// serialization order when needed.
86+
pub(super) fn sort(&mut self) {
87+
self.localized
88+
.sort_unstable_by(|k1, _v1, k2, _v2| k1.as_str().cmp(k2.as_str()));
89+
}
8390
}
8491

8592
#[serde_as]

crates/oauth2-types/src/registration/mod.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,51 @@ impl ClientMetadata {
564564
Ok(VerifiedClientMetadata { inner: self })
565565
}
566566

567+
/// Sort the properties. This is inteded to ensure a stable serialization
568+
/// order when needed.
569+
#[must_use]
570+
pub fn sorted(mut self) -> Self {
571+
// This sorts all the Vec<T> and Localized<T> fields
572+
if let Some(redirect_uris) = &mut self.redirect_uris {
573+
redirect_uris.sort();
574+
}
575+
if let Some(response_types) = &mut self.response_types {
576+
response_types.sort();
577+
}
578+
if let Some(grant_types) = &mut self.grant_types {
579+
grant_types.sort();
580+
}
581+
if let Some(contacts) = &mut self.contacts {
582+
contacts.sort();
583+
}
584+
if let Some(client_name) = &mut self.client_name {
585+
client_name.sort();
586+
}
587+
if let Some(logo_uri) = &mut self.logo_uri {
588+
logo_uri.sort();
589+
}
590+
if let Some(client_uri) = &mut self.client_uri {
591+
client_uri.sort();
592+
}
593+
if let Some(policy_uri) = &mut self.policy_uri {
594+
policy_uri.sort();
595+
}
596+
if let Some(tos_uri) = &mut self.tos_uri {
597+
tos_uri.sort();
598+
}
599+
if let Some(default_acr_values) = &mut self.default_acr_values {
600+
default_acr_values.sort();
601+
}
602+
if let Some(request_uris) = &mut self.request_uris {
603+
request_uris.sort();
604+
}
605+
if let Some(post_logout_redirect_uris) = &mut self.post_logout_redirect_uris {
606+
post_logout_redirect_uris.sort();
607+
}
608+
609+
self
610+
}
611+
567612
/// Array of the [OAuth 2.0 `response_type` values] that the client can use
568613
/// at the [authorization endpoint].
569614
///

crates/oauth2-types/src/response_type.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ impl core::str::FromStr for ResponseTypeToken {
7878
///
7979
/// [OAuth 2.0 `response_type` value]: https://www.rfc-editor.org/rfc/rfc7591#page-9
8080
/// [authorization endpoint]: https://www.rfc-editor.org/rfc/rfc6749.html#section-3.1
81-
#[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)]
81+
#[derive(Debug, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr, PartialOrd, Ord)]
8282
pub struct ResponseType(BTreeSet<ResponseTypeToken>);
8383

8484
impl std::ops::Deref for ResponseType {

0 commit comments

Comments
 (0)