1use 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 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 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}