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