oauth2_types/registration/
client_metadata_serde.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use std::borrow::Cow;
8
9use chrono::Duration;
10use indexmap::IndexMap;
11use language_tags::LanguageTag;
12use mas_iana::{
13    jose::{JsonWebEncryptionAlg, JsonWebEncryptionEnc, JsonWebSignatureAlg},
14    oauth::OAuthClientAuthenticationMethod,
15};
16use mas_jose::jwk::PublicJsonWebKeySet;
17use serde::{
18    Deserialize, Serialize,
19    de::{DeserializeOwned, Error},
20    ser::SerializeMap,
21};
22use serde_json::Value;
23use serde_with::{DurationSeconds, serde_as, skip_serializing_none};
24use url::Url;
25
26use super::{ClientMetadata, Localized, VerifiedClientMetadata};
27use crate::{
28    oidc::{ApplicationType, SubjectType},
29    requests::GrantType,
30    response_type::ResponseType,
31};
32
33impl<T> Localized<T> {
34    fn serialize<M>(&self, map: &mut M, field_name: &str) -> Result<(), M::Error>
35    where
36        M: SerializeMap,
37        T: Serialize,
38    {
39        map.serialize_entry(field_name, &self.non_localized)?;
40
41        for (lang, localized) in &self.localized {
42            map.serialize_entry(&format!("{field_name}#{lang}"), localized)?;
43        }
44
45        Ok(())
46    }
47
48    fn deserialize(
49        map: &mut IndexMap<String, IndexMap<Option<LanguageTag>, Value>>,
50        field_name: &'static str,
51    ) -> Result<Option<Self>, serde_json::Error>
52    where
53        T: DeserializeOwned,
54    {
55        let Some(map) = map.shift_remove(field_name) else {
56            return Ok(None);
57        };
58
59        let mut non_localized = None;
60        let mut localized = IndexMap::with_capacity(map.len() - 1);
61
62        for (k, v) in map {
63            let value = serde_json::from_value(v)?;
64
65            if let Some(lang) = k {
66                localized.insert(lang, value);
67            } else {
68                non_localized = Some(value);
69            }
70        }
71
72        let non_localized = non_localized.ok_or_else(|| {
73            serde_json::Error::custom(format!(
74                "missing non-localized variant of field '{field_name}'"
75            ))
76        })?;
77
78        Ok(Some(Localized {
79            non_localized,
80            localized,
81        }))
82    }
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    }
90}
91
92#[serde_as]
93#[skip_serializing_none]
94#[derive(Serialize, Deserialize)]
95pub struct ClientMetadataSerdeHelper {
96    redirect_uris: Option<Vec<Url>>,
97    response_types: Option<Vec<ResponseType>>,
98    grant_types: Option<Vec<GrantType>>,
99    application_type: Option<ApplicationType>,
100    contacts: Option<Vec<String>>,
101    jwks_uri: Option<Url>,
102    jwks: Option<PublicJsonWebKeySet>,
103    software_id: Option<String>,
104    software_version: Option<String>,
105    sector_identifier_uri: Option<Url>,
106    subject_type: Option<SubjectType>,
107    token_endpoint_auth_method: Option<OAuthClientAuthenticationMethod>,
108    token_endpoint_auth_signing_alg: Option<JsonWebSignatureAlg>,
109    id_token_signed_response_alg: Option<JsonWebSignatureAlg>,
110    id_token_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
111    id_token_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
112    userinfo_signed_response_alg: Option<JsonWebSignatureAlg>,
113    userinfo_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
114    userinfo_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
115    request_object_signing_alg: Option<JsonWebSignatureAlg>,
116    request_object_encryption_alg: Option<JsonWebEncryptionAlg>,
117    request_object_encryption_enc: Option<JsonWebEncryptionEnc>,
118    #[serde_as(as = "Option<DurationSeconds<i64>>")]
119    default_max_age: Option<Duration>,
120    require_auth_time: Option<bool>,
121    default_acr_values: Option<Vec<String>>,
122    initiate_login_uri: Option<Url>,
123    request_uris: Option<Vec<Url>>,
124    require_signed_request_object: Option<bool>,
125    require_pushed_authorization_requests: Option<bool>,
126    introspection_signed_response_alg: Option<JsonWebSignatureAlg>,
127    introspection_encrypted_response_alg: Option<JsonWebEncryptionAlg>,
128    introspection_encrypted_response_enc: Option<JsonWebEncryptionEnc>,
129    post_logout_redirect_uris: Option<Vec<Url>>,
130    #[serde(flatten)]
131    extra: ClientMetadataLocalizedFields,
132}
133
134impl From<VerifiedClientMetadata> for ClientMetadataSerdeHelper {
135    fn from(metadata: VerifiedClientMetadata) -> Self {
136        metadata.inner.into()
137    }
138}
139
140impl From<ClientMetadata> for ClientMetadataSerdeHelper {
141    fn from(metadata: ClientMetadata) -> Self {
142        let ClientMetadata {
143            redirect_uris,
144            response_types,
145            grant_types,
146            application_type,
147            contacts,
148            client_name,
149            logo_uri,
150            client_uri,
151            policy_uri,
152            tos_uri,
153            jwks_uri,
154            jwks,
155            software_id,
156            software_version,
157            sector_identifier_uri,
158            subject_type,
159            token_endpoint_auth_method,
160            token_endpoint_auth_signing_alg,
161            id_token_signed_response_alg,
162            id_token_encrypted_response_alg,
163            id_token_encrypted_response_enc,
164            userinfo_signed_response_alg,
165            userinfo_encrypted_response_alg,
166            userinfo_encrypted_response_enc,
167            request_object_signing_alg,
168            request_object_encryption_alg,
169            request_object_encryption_enc,
170            default_max_age,
171            require_auth_time,
172            default_acr_values,
173            initiate_login_uri,
174            request_uris,
175            require_signed_request_object,
176            require_pushed_authorization_requests,
177            introspection_signed_response_alg,
178            introspection_encrypted_response_alg,
179            introspection_encrypted_response_enc,
180            post_logout_redirect_uris,
181        } = metadata;
182
183        ClientMetadataSerdeHelper {
184            redirect_uris,
185            response_types,
186            grant_types,
187            application_type,
188            contacts,
189            jwks_uri,
190            jwks,
191            software_id,
192            software_version,
193            sector_identifier_uri,
194            subject_type,
195            token_endpoint_auth_method,
196            token_endpoint_auth_signing_alg,
197            id_token_signed_response_alg,
198            id_token_encrypted_response_alg,
199            id_token_encrypted_response_enc,
200            userinfo_signed_response_alg,
201            userinfo_encrypted_response_alg,
202            userinfo_encrypted_response_enc,
203            request_object_signing_alg,
204            request_object_encryption_alg,
205            request_object_encryption_enc,
206            default_max_age,
207            require_auth_time,
208            default_acr_values,
209            initiate_login_uri,
210            request_uris,
211            require_signed_request_object,
212            require_pushed_authorization_requests,
213            introspection_signed_response_alg,
214            introspection_encrypted_response_alg,
215            introspection_encrypted_response_enc,
216            post_logout_redirect_uris,
217            extra: ClientMetadataLocalizedFields {
218                client_name,
219                logo_uri,
220                client_uri,
221                policy_uri,
222                tos_uri,
223            },
224        }
225    }
226}
227
228impl From<ClientMetadataSerdeHelper> for ClientMetadata {
229    fn from(metadata: ClientMetadataSerdeHelper) -> Self {
230        let ClientMetadataSerdeHelper {
231            redirect_uris,
232            response_types,
233            grant_types,
234            application_type,
235            contacts,
236            jwks_uri,
237            jwks,
238            software_id,
239            software_version,
240            sector_identifier_uri,
241            subject_type,
242            token_endpoint_auth_method,
243            token_endpoint_auth_signing_alg,
244            id_token_signed_response_alg,
245            id_token_encrypted_response_alg,
246            id_token_encrypted_response_enc,
247            userinfo_signed_response_alg,
248            userinfo_encrypted_response_alg,
249            userinfo_encrypted_response_enc,
250            request_object_signing_alg,
251            request_object_encryption_alg,
252            request_object_encryption_enc,
253            default_max_age,
254            require_auth_time,
255            default_acr_values,
256            initiate_login_uri,
257            request_uris,
258            require_signed_request_object,
259            require_pushed_authorization_requests,
260            introspection_signed_response_alg,
261            introspection_encrypted_response_alg,
262            introspection_encrypted_response_enc,
263            post_logout_redirect_uris,
264            extra:
265                ClientMetadataLocalizedFields {
266                    client_name,
267                    logo_uri,
268                    client_uri,
269                    policy_uri,
270                    tos_uri,
271                },
272        } = metadata;
273
274        ClientMetadata {
275            redirect_uris,
276            response_types,
277            grant_types,
278            application_type,
279            contacts,
280            client_name,
281            logo_uri,
282            client_uri,
283            policy_uri,
284            tos_uri,
285            jwks_uri,
286            jwks,
287            software_id,
288            software_version,
289            sector_identifier_uri,
290            subject_type,
291            token_endpoint_auth_method,
292            token_endpoint_auth_signing_alg,
293            id_token_signed_response_alg,
294            id_token_encrypted_response_alg,
295            id_token_encrypted_response_enc,
296            userinfo_signed_response_alg,
297            userinfo_encrypted_response_alg,
298            userinfo_encrypted_response_enc,
299            request_object_signing_alg,
300            request_object_encryption_alg,
301            request_object_encryption_enc,
302            default_max_age,
303            require_auth_time,
304            default_acr_values,
305            initiate_login_uri,
306            request_uris,
307            require_signed_request_object,
308            require_pushed_authorization_requests,
309            introspection_signed_response_alg,
310            introspection_encrypted_response_alg,
311            introspection_encrypted_response_enc,
312            post_logout_redirect_uris,
313        }
314    }
315}
316
317struct ClientMetadataLocalizedFields {
318    client_name: Option<Localized<String>>,
319    logo_uri: Option<Localized<Url>>,
320    client_uri: Option<Localized<Url>>,
321    policy_uri: Option<Localized<Url>>,
322    tos_uri: Option<Localized<Url>>,
323}
324
325impl Serialize for ClientMetadataLocalizedFields {
326    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
327    where
328        S: serde::Serializer,
329    {
330        let mut map = serializer.serialize_map(None)?;
331
332        if let Some(client_name) = &self.client_name {
333            client_name.serialize(&mut map, "client_name")?;
334        }
335
336        if let Some(logo_uri) = &self.logo_uri {
337            logo_uri.serialize(&mut map, "logo_uri")?;
338        }
339
340        if let Some(client_uri) = &self.client_uri {
341            client_uri.serialize(&mut map, "client_uri")?;
342        }
343
344        if let Some(policy_uri) = &self.policy_uri {
345            policy_uri.serialize(&mut map, "policy_uri")?;
346        }
347
348        if let Some(tos_uri) = &self.tos_uri {
349            tos_uri.serialize(&mut map, "tos_uri")?;
350        }
351
352        map.end()
353    }
354}
355
356impl<'de> Deserialize<'de> for ClientMetadataLocalizedFields {
357    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
358    where
359        D: serde::Deserializer<'de>,
360    {
361        let map = IndexMap::<Cow<'de, str>, Value>::deserialize(deserializer)?;
362        let mut new_map: IndexMap<String, IndexMap<Option<LanguageTag>, Value>> = IndexMap::new();
363
364        for (k, v) in map {
365            let (prefix, lang) = if let Some((prefix, lang)) = k.split_once('#') {
366                let lang = LanguageTag::parse(lang).map_err(|_| {
367                    D::Error::invalid_value(serde::de::Unexpected::Str(lang), &"language tag")
368                })?;
369                (prefix.to_owned(), Some(lang))
370            } else {
371                (k.into_owned(), None)
372            };
373
374            new_map.entry(prefix).or_default().insert(lang, v);
375        }
376
377        let client_name =
378            Localized::deserialize(&mut new_map, "client_name").map_err(D::Error::custom)?;
379
380        let logo_uri =
381            Localized::deserialize(&mut new_map, "logo_uri").map_err(D::Error::custom)?;
382
383        let client_uri =
384            Localized::deserialize(&mut new_map, "client_uri").map_err(D::Error::custom)?;
385
386        let policy_uri =
387            Localized::deserialize(&mut new_map, "policy_uri").map_err(D::Error::custom)?;
388
389        let tos_uri = Localized::deserialize(&mut new_map, "tos_uri").map_err(D::Error::custom)?;
390
391        Ok(Self {
392            client_name,
393            logo_uri,
394            client_uri,
395            policy_uri,
396            tos_uri,
397        })
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use insta::assert_yaml_snapshot;
404
405    use super::*;
406
407    #[test]
408    fn deserialize_localized_fields() {
409        let metadata = serde_json::json!({
410            "redirect_uris": ["http://localhost/oidc"],
411            "client_name": "Postbox",
412            "client_name#fr": "Boîte à lettres",
413            "client_uri": "https://localhost/",
414            "client_uri#fr": "https://localhost/fr",
415            "client_uri#de": "https://localhost/de",
416        });
417
418        let metadata: ClientMetadata = serde_json::from_value(metadata).unwrap();
419
420        let name = metadata.client_name.unwrap();
421        assert_eq!(name.non_localized(), "Postbox");
422        assert_eq!(
423            name.get(Some(&LanguageTag::parse("fr").unwrap())).unwrap(),
424            "Boîte à lettres"
425        );
426        assert_eq!(name.get(Some(&LanguageTag::parse("de").unwrap())), None);
427
428        let client_uri = metadata.client_uri.unwrap();
429        assert_eq!(client_uri.non_localized().as_ref(), "https://localhost/");
430        assert_eq!(
431            client_uri
432                .get(Some(&LanguageTag::parse("fr").unwrap()))
433                .unwrap()
434                .as_ref(),
435            "https://localhost/fr"
436        );
437        assert_eq!(
438            client_uri
439                .get(Some(&LanguageTag::parse("de").unwrap()))
440                .unwrap()
441                .as_ref(),
442            "https://localhost/de"
443        );
444    }
445
446    #[test]
447    fn serialize_localized_fields() {
448        let client_name = Localized::new(
449            "Postbox".to_owned(),
450            [(
451                LanguageTag::parse("fr").unwrap(),
452                "Boîte à lettres".to_owned(),
453            )],
454        );
455        let client_uri = Localized::new(
456            Url::parse("https://localhost").unwrap(),
457            [
458                (
459                    LanguageTag::parse("fr").unwrap(),
460                    Url::parse("https://localhost/fr").unwrap(),
461                ),
462                (
463                    LanguageTag::parse("de").unwrap(),
464                    Url::parse("https://localhost/de").unwrap(),
465                ),
466            ],
467        );
468        let metadata = ClientMetadata {
469            redirect_uris: Some(vec![Url::parse("http://localhost/oidc").unwrap()]),
470            client_name: Some(client_name),
471            client_uri: Some(client_uri),
472            ..Default::default()
473        }
474        .validate()
475        .unwrap();
476
477        assert_yaml_snapshot!(metadata, @r###"
478        redirect_uris:
479          - "http://localhost/oidc"
480        client_name: Postbox
481        "client_name#fr": Boîte à lettres
482        client_uri: "https://localhost/"
483        "client_uri#fr": "https://localhost/fr"
484        "client_uri#de": "https://localhost/de"
485        "###);
486
487        // Do a roundtrip, we should get the same metadata back with the same order
488        let metadata: ClientMetadata =
489            serde_json::from_value(serde_json::to_value(metadata).unwrap()).unwrap();
490        let metadata = metadata.validate().unwrap();
491        assert_yaml_snapshot!(metadata, @r###"
492        redirect_uris:
493          - "http://localhost/oidc"
494        client_name: Postbox
495        "client_name#fr": Boîte à lettres
496        client_uri: "https://localhost/"
497        "client_uri#fr": "https://localhost/fr"
498        "client_uri#de": "https://localhost/de"
499        "###);
500    }
501}