ruma_client_api/discovery/get_authorization_server_metadata/
serde.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, de};
4use url::Url;
5
6#[cfg(feature = "unstable-msc4191")]
7use super::v1::AccountManagementAction;
8use super::v1::{
9    AuthorizationServerMetadata, CodeChallengeMethod, GrantType, Prompt, ResponseMode, ResponseType,
10};
11
12impl<'de> Deserialize<'de> for AuthorizationServerMetadata {
13    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
14    where
15        D: serde::Deserializer<'de>,
16    {
17        let helper = AuthorizationServerMetadataDeHelper::deserialize(deserializer)?;
18
19        let AuthorizationServerMetadataDeHelper {
20            issuer,
21            authorization_endpoint,
22            token_endpoint,
23            registration_endpoint,
24            response_types_supported,
25            response_modes_supported,
26            grant_types_supported,
27            revocation_endpoint,
28            code_challenge_methods_supported,
29            #[cfg(feature = "unstable-msc4191")]
30            account_management_uri,
31            #[cfg(feature = "unstable-msc4191")]
32            account_management_actions_supported,
33            #[cfg(feature = "unstable-msc4108")]
34            device_authorization_endpoint,
35            prompt_values_supported,
36        } = helper;
37
38        // Require `code` in `response_types_supported`.
39        if !response_types_supported.contains(&ResponseType::Code) {
40            return Err(de::Error::custom("missing value `code` in `response_types_supported`"));
41        }
42
43        // Require `query` and `fragment` in `response_modes_supported`.
44        if let Some(response_modes) = &response_modes_supported {
45            let query_found = response_modes.contains(&ResponseMode::Query);
46            let fragment_found = response_modes.contains(&ResponseMode::Fragment);
47
48            if !query_found && !fragment_found {
49                return Err(de::Error::custom(
50                    "missing values `query` and `fragment` in `response_modes_supported`",
51                ));
52            }
53            if !query_found {
54                return Err(de::Error::custom(
55                    "missing value `query` in `response_modes_supported`",
56                ));
57            }
58            if !fragment_found {
59                return Err(de::Error::custom(
60                    "missing value `fragment` in `response_modes_supported`",
61                ));
62            }
63        }
64        // If the field is missing, the default value is `["query", "fragment"]`, according to
65        // RFC8414.
66        let response_modes_supported = response_modes_supported
67            .unwrap_or_else(|| [ResponseMode::Query, ResponseMode::Fragment].into());
68
69        // Require `authorization_code` and `refresh_token` in `grant_types_supported`.
70        let authorization_code_found =
71            grant_types_supported.contains(&GrantType::AuthorizationCode);
72        let refresh_token_found = grant_types_supported.contains(&GrantType::RefreshToken);
73        if !authorization_code_found && !refresh_token_found {
74            return Err(de::Error::custom(
75                "missing values `authorization_code` and `refresh_token` in `grant_types_supported`",
76            ));
77        }
78        if !authorization_code_found {
79            return Err(de::Error::custom(
80                "missing value `authorization_code` in `grant_types_supported`",
81            ));
82        }
83        if !refresh_token_found {
84            return Err(de::Error::custom(
85                "missing value `refresh_token` in `grant_types_supported`",
86            ));
87        }
88
89        // Require `S256` in `code_challenge_methods_supported`.
90        if !code_challenge_methods_supported.contains(&CodeChallengeMethod::S256) {
91            return Err(de::Error::custom(
92                "missing value `S256` in `code_challenge_methods_supported`",
93            ));
94        }
95
96        Ok(AuthorizationServerMetadata {
97            issuer,
98            authorization_endpoint,
99            token_endpoint,
100            registration_endpoint,
101            response_types_supported,
102            response_modes_supported,
103            grant_types_supported,
104            revocation_endpoint,
105            code_challenge_methods_supported,
106            #[cfg(feature = "unstable-msc4191")]
107            account_management_uri,
108            #[cfg(feature = "unstable-msc4191")]
109            account_management_actions_supported,
110            #[cfg(feature = "unstable-msc4108")]
111            device_authorization_endpoint,
112            prompt_values_supported,
113        })
114    }
115}
116
117#[derive(Deserialize)]
118struct AuthorizationServerMetadataDeHelper {
119    issuer: Url,
120    authorization_endpoint: Url,
121    token_endpoint: Url,
122    registration_endpoint: Option<Url>,
123    response_types_supported: BTreeSet<ResponseType>,
124    response_modes_supported: Option<BTreeSet<ResponseMode>>,
125    grant_types_supported: BTreeSet<GrantType>,
126    revocation_endpoint: Url,
127    code_challenge_methods_supported: BTreeSet<CodeChallengeMethod>,
128    #[cfg(feature = "unstable-msc4191")]
129    account_management_uri: Option<Url>,
130    #[cfg(feature = "unstable-msc4191")]
131    #[serde(default)]
132    account_management_actions_supported: BTreeSet<AccountManagementAction>,
133    #[cfg(feature = "unstable-msc4108")]
134    device_authorization_endpoint: Option<Url>,
135    #[serde(default)]
136    prompt_values_supported: Vec<Prompt>,
137}
138
139#[cfg(test)]
140mod tests {
141    use as_variant::as_variant;
142    use serde_json::{Value as JsonValue, from_value as from_json_value, value::Map as JsonMap};
143    use url::Url;
144
145    #[cfg(feature = "unstable-msc4191")]
146    use crate::discovery::get_authorization_server_metadata::v1::AccountManagementAction;
147    use crate::discovery::get_authorization_server_metadata::{
148        tests::authorization_server_metadata_json,
149        v1::{
150            AuthorizationServerMetadata, CodeChallengeMethod, GrantType, ResponseMode, ResponseType,
151        },
152    };
153
154    /// A valid `AuthorizationServerMetadata` with all fields and values, as a JSON object.
155    fn authorization_server_metadata_object() -> JsonMap<String, JsonValue> {
156        as_variant!(authorization_server_metadata_json(), JsonValue::Object).unwrap()
157    }
158
159    /// Get a mutable reference to the array value with the given key in the given object.
160    ///
161    /// Panics if the property doesn't exist or is not an array.
162    fn get_mut_array<'a>(
163        object: &'a mut JsonMap<String, JsonValue>,
164        key: &str,
165    ) -> &'a mut Vec<JsonValue> {
166        object.get_mut(key).unwrap().as_array_mut().unwrap()
167    }
168
169    #[test]
170    fn metadata_all_fields() {
171        let metadata_object = authorization_server_metadata_object();
172        let metadata =
173            from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
174
175        assert_eq!(metadata.issuer.as_str(), "https://server.local/");
176        assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
177        assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
178        assert_eq!(
179            metadata.registration_endpoint.as_ref().map(Url::as_str),
180            Some("https://server.local/register")
181        );
182
183        assert_eq!(metadata.response_types_supported.len(), 1);
184        assert!(metadata.response_types_supported.contains(&ResponseType::Code));
185
186        assert_eq!(metadata.response_modes_supported.len(), 2);
187        assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
188        assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
189
190        assert_eq!(metadata.grant_types_supported.len(), 2);
191        assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
192        assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
193
194        assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
195
196        assert_eq!(metadata.code_challenge_methods_supported.len(), 1);
197        assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
198
199        #[cfg(feature = "unstable-msc4191")]
200        {
201            assert_eq!(
202                metadata.account_management_uri.as_ref().map(Url::as_str),
203                Some("https://server.local/account")
204            );
205            assert_eq!(metadata.account_management_actions_supported.len(), 6);
206            assert!(
207                metadata
208                    .account_management_actions_supported
209                    .contains(&AccountManagementAction::Profile)
210            );
211            assert!(
212                metadata
213                    .account_management_actions_supported
214                    .contains(&AccountManagementAction::SessionsList)
215            );
216            assert!(
217                metadata
218                    .account_management_actions_supported
219                    .contains(&AccountManagementAction::SessionView)
220            );
221            assert!(
222                metadata
223                    .account_management_actions_supported
224                    .contains(&AccountManagementAction::SessionEnd)
225            );
226            assert!(
227                metadata
228                    .account_management_actions_supported
229                    .contains(&AccountManagementAction::AccountDeactivate)
230            );
231            assert!(
232                metadata
233                    .account_management_actions_supported
234                    .contains(&AccountManagementAction::CrossSigningReset)
235            );
236        }
237
238        #[cfg(feature = "unstable-msc4108")]
239        assert_eq!(
240            metadata.device_authorization_endpoint.as_ref().map(Url::as_str),
241            Some("https://server.local/device")
242        );
243    }
244
245    #[test]
246    fn metadata_no_optional_fields() {
247        let mut metadata_object = authorization_server_metadata_object();
248        assert!(metadata_object.remove("registration_endpoint").is_some());
249        assert!(metadata_object.remove("response_modes_supported").is_some());
250        assert!(metadata_object.remove("account_management_uri").is_some());
251        assert!(metadata_object.remove("account_management_actions_supported").is_some());
252        assert!(metadata_object.remove("device_authorization_endpoint").is_some());
253
254        let metadata =
255            from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
256
257        assert_eq!(metadata.issuer.as_str(), "https://server.local/");
258        assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
259        assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
260        assert_eq!(metadata.registration_endpoint, None);
261
262        assert_eq!(metadata.response_types_supported.len(), 1);
263        assert!(metadata.response_types_supported.contains(&ResponseType::Code));
264
265        assert_eq!(metadata.response_modes_supported.len(), 2);
266        assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
267        assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
268
269        assert_eq!(metadata.grant_types_supported.len(), 2);
270        assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
271        assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
272
273        assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
274
275        assert_eq!(metadata.code_challenge_methods_supported.len(), 1);
276        assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
277
278        #[cfg(feature = "unstable-msc4191")]
279        {
280            assert_eq!(metadata.account_management_uri, None);
281            assert_eq!(metadata.account_management_actions_supported.len(), 0);
282        }
283
284        #[cfg(feature = "unstable-msc4108")]
285        assert_eq!(metadata.device_authorization_endpoint, None);
286    }
287
288    #[test]
289    fn metadata_additional_values() {
290        let mut metadata_object = authorization_server_metadata_object();
291        get_mut_array(&mut metadata_object, "response_types_supported").push("custom".into());
292        get_mut_array(&mut metadata_object, "response_modes_supported").push("custom".into());
293        get_mut_array(&mut metadata_object, "grant_types_supported").push("custom".into());
294        get_mut_array(&mut metadata_object, "code_challenge_methods_supported")
295            .push("custom".into());
296        get_mut_array(&mut metadata_object, "account_management_actions_supported")
297            .push("custom".into());
298
299        let metadata =
300            from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
301
302        assert_eq!(metadata.issuer.as_str(), "https://server.local/");
303        assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
304        assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
305        assert_eq!(
306            metadata.registration_endpoint.as_ref().map(Url::as_str),
307            Some("https://server.local/register")
308        );
309
310        assert_eq!(metadata.response_types_supported.len(), 2);
311        assert!(metadata.response_types_supported.contains(&ResponseType::Code));
312        assert!(metadata.response_types_supported.contains(&ResponseType::from("custom")));
313
314        assert_eq!(metadata.response_modes_supported.len(), 3);
315        assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
316        assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
317        assert!(metadata.response_modes_supported.contains(&ResponseMode::from("custom")));
318
319        assert_eq!(metadata.grant_types_supported.len(), 3);
320        assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
321        assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
322        assert!(metadata.grant_types_supported.contains(&GrantType::from("custom")));
323
324        assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
325
326        assert_eq!(metadata.code_challenge_methods_supported.len(), 2);
327        assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
328        assert!(
329            metadata
330                .code_challenge_methods_supported
331                .contains(&CodeChallengeMethod::from("custom"))
332        );
333
334        #[cfg(feature = "unstable-msc4191")]
335        {
336            assert_eq!(
337                metadata.account_management_uri.as_ref().map(Url::as_str),
338                Some("https://server.local/account")
339            );
340            assert_eq!(metadata.account_management_actions_supported.len(), 7);
341            assert!(
342                metadata
343                    .account_management_actions_supported
344                    .contains(&AccountManagementAction::Profile)
345            );
346            assert!(
347                metadata
348                    .account_management_actions_supported
349                    .contains(&AccountManagementAction::SessionsList)
350            );
351            assert!(
352                metadata
353                    .account_management_actions_supported
354                    .contains(&AccountManagementAction::SessionView)
355            );
356            assert!(
357                metadata
358                    .account_management_actions_supported
359                    .contains(&AccountManagementAction::SessionEnd)
360            );
361            assert!(
362                metadata
363                    .account_management_actions_supported
364                    .contains(&AccountManagementAction::AccountDeactivate)
365            );
366            assert!(
367                metadata
368                    .account_management_actions_supported
369                    .contains(&AccountManagementAction::CrossSigningReset)
370            );
371            assert!(
372                metadata
373                    .account_management_actions_supported
374                    .contains(&AccountManagementAction::from("custom"))
375            );
376        }
377
378        #[cfg(feature = "unstable-msc4108")]
379        assert_eq!(
380            metadata.device_authorization_endpoint.as_ref().map(Url::as_str),
381            Some("https://server.local/device")
382        );
383    }
384
385    #[test]
386    fn metadata_missing_required_fields() {
387        let original_metadata_object = authorization_server_metadata_object();
388
389        let mut metadata_object = original_metadata_object.clone();
390        assert!(metadata_object.remove("issuer").is_some());
391        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
392
393        let mut metadata_object = original_metadata_object.clone();
394        assert!(metadata_object.remove("authorization_endpoint").is_some());
395        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
396
397        let mut metadata_object = original_metadata_object.clone();
398        assert!(metadata_object.remove("token_endpoint").is_some());
399        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
400
401        let mut metadata_object = original_metadata_object.clone();
402        assert!(metadata_object.remove("response_types_supported").is_some());
403        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
404
405        let mut metadata_object = original_metadata_object.clone();
406        assert!(metadata_object.remove("grant_types_supported").is_some());
407        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
408
409        let mut metadata_object = original_metadata_object.clone();
410        assert!(metadata_object.remove("revocation_endpoint").is_some());
411        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
412
413        let mut metadata_object = original_metadata_object;
414        assert!(metadata_object.remove("code_challenge_methods_supported").is_some());
415        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
416    }
417
418    #[test]
419    fn metadata_missing_required_values() {
420        let original_metadata_object = authorization_server_metadata_object();
421
422        let mut metadata_object = original_metadata_object.clone();
423        get_mut_array(&mut metadata_object, "response_types_supported").clear();
424        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
425
426        let mut metadata_object = original_metadata_object.clone();
427        get_mut_array(&mut metadata_object, "response_modes_supported").clear();
428        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
429
430        let mut metadata_object = original_metadata_object.clone();
431        get_mut_array(&mut metadata_object, "response_modes_supported").remove(0);
432        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
433
434        let mut metadata_object = original_metadata_object.clone();
435        get_mut_array(&mut metadata_object, "response_modes_supported").remove(1);
436        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
437
438        let mut metadata_object = original_metadata_object.clone();
439        get_mut_array(&mut metadata_object, "grant_types_supported").clear();
440        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
441
442        let mut metadata_object = original_metadata_object.clone();
443        get_mut_array(&mut metadata_object, "grant_types_supported").remove(0);
444        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
445
446        let mut metadata_object = original_metadata_object.clone();
447        get_mut_array(&mut metadata_object, "grant_types_supported").remove(1);
448        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
449
450        let mut metadata_object = original_metadata_object;
451        get_mut_array(&mut metadata_object, "code_challenge_methods_supported").clear();
452        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
453    }
454}