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