ruma_client_api/discovery/get_authorization_server_metadata/
serde.rs

1use std::collections::BTreeSet;
2
3use serde::{de, Deserialize};
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::{from_value as from_json_value, value::Map as JsonMap, Value as JsonValue};
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!(metadata
207                .account_management_actions_supported
208                .contains(&AccountManagementAction::Profile));
209            assert!(metadata
210                .account_management_actions_supported
211                .contains(&AccountManagementAction::SessionsList));
212            assert!(metadata
213                .account_management_actions_supported
214                .contains(&AccountManagementAction::SessionView));
215            assert!(metadata
216                .account_management_actions_supported
217                .contains(&AccountManagementAction::SessionEnd));
218            assert!(metadata
219                .account_management_actions_supported
220                .contains(&AccountManagementAction::AccountDeactivate));
221            assert!(metadata
222                .account_management_actions_supported
223                .contains(&AccountManagementAction::CrossSigningReset));
224        }
225
226        #[cfg(feature = "unstable-msc4108")]
227        assert_eq!(
228            metadata.device_authorization_endpoint.as_ref().map(Url::as_str),
229            Some("https://server.local/device")
230        );
231    }
232
233    #[test]
234    fn metadata_no_optional_fields() {
235        let mut metadata_object = authorization_server_metadata_object();
236        assert!(metadata_object.remove("registration_endpoint").is_some());
237        assert!(metadata_object.remove("response_modes_supported").is_some());
238        assert!(metadata_object.remove("account_management_uri").is_some());
239        assert!(metadata_object.remove("account_management_actions_supported").is_some());
240        assert!(metadata_object.remove("device_authorization_endpoint").is_some());
241
242        let metadata =
243            from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
244
245        assert_eq!(metadata.issuer.as_str(), "https://server.local/");
246        assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
247        assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
248        assert_eq!(metadata.registration_endpoint, None);
249
250        assert_eq!(metadata.response_types_supported.len(), 1);
251        assert!(metadata.response_types_supported.contains(&ResponseType::Code));
252
253        assert_eq!(metadata.response_modes_supported.len(), 2);
254        assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
255        assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
256
257        assert_eq!(metadata.grant_types_supported.len(), 2);
258        assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
259        assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
260
261        assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
262
263        assert_eq!(metadata.code_challenge_methods_supported.len(), 1);
264        assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
265
266        #[cfg(feature = "unstable-msc4191")]
267        {
268            assert_eq!(metadata.account_management_uri, None);
269            assert_eq!(metadata.account_management_actions_supported.len(), 0);
270        }
271
272        #[cfg(feature = "unstable-msc4108")]
273        assert_eq!(metadata.device_authorization_endpoint, None);
274    }
275
276    #[test]
277    fn metadata_additional_values() {
278        let mut metadata_object = authorization_server_metadata_object();
279        get_mut_array(&mut metadata_object, "response_types_supported").push("custom".into());
280        get_mut_array(&mut metadata_object, "response_modes_supported").push("custom".into());
281        get_mut_array(&mut metadata_object, "grant_types_supported").push("custom".into());
282        get_mut_array(&mut metadata_object, "code_challenge_methods_supported")
283            .push("custom".into());
284        get_mut_array(&mut metadata_object, "account_management_actions_supported")
285            .push("custom".into());
286
287        let metadata =
288            from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap();
289
290        assert_eq!(metadata.issuer.as_str(), "https://server.local/");
291        assert_eq!(metadata.authorization_endpoint.as_str(), "https://server.local/authorize");
292        assert_eq!(metadata.token_endpoint.as_str(), "https://server.local/token");
293        assert_eq!(
294            metadata.registration_endpoint.as_ref().map(Url::as_str),
295            Some("https://server.local/register")
296        );
297
298        assert_eq!(metadata.response_types_supported.len(), 2);
299        assert!(metadata.response_types_supported.contains(&ResponseType::Code));
300        assert!(metadata.response_types_supported.contains(&ResponseType::from("custom")));
301
302        assert_eq!(metadata.response_modes_supported.len(), 3);
303        assert!(metadata.response_modes_supported.contains(&ResponseMode::Query));
304        assert!(metadata.response_modes_supported.contains(&ResponseMode::Fragment));
305        assert!(metadata.response_modes_supported.contains(&ResponseMode::from("custom")));
306
307        assert_eq!(metadata.grant_types_supported.len(), 3);
308        assert!(metadata.grant_types_supported.contains(&GrantType::AuthorizationCode));
309        assert!(metadata.grant_types_supported.contains(&GrantType::RefreshToken));
310        assert!(metadata.grant_types_supported.contains(&GrantType::from("custom")));
311
312        assert_eq!(metadata.revocation_endpoint.as_str(), "https://server.local/revoke");
313
314        assert_eq!(metadata.code_challenge_methods_supported.len(), 2);
315        assert!(metadata.code_challenge_methods_supported.contains(&CodeChallengeMethod::S256));
316        assert!(metadata
317            .code_challenge_methods_supported
318            .contains(&CodeChallengeMethod::from("custom")));
319
320        #[cfg(feature = "unstable-msc4191")]
321        {
322            assert_eq!(
323                metadata.account_management_uri.as_ref().map(Url::as_str),
324                Some("https://server.local/account")
325            );
326            assert_eq!(metadata.account_management_actions_supported.len(), 7);
327            assert!(metadata
328                .account_management_actions_supported
329                .contains(&AccountManagementAction::Profile));
330            assert!(metadata
331                .account_management_actions_supported
332                .contains(&AccountManagementAction::SessionsList));
333            assert!(metadata
334                .account_management_actions_supported
335                .contains(&AccountManagementAction::SessionView));
336            assert!(metadata
337                .account_management_actions_supported
338                .contains(&AccountManagementAction::SessionEnd));
339            assert!(metadata
340                .account_management_actions_supported
341                .contains(&AccountManagementAction::AccountDeactivate));
342            assert!(metadata
343                .account_management_actions_supported
344                .contains(&AccountManagementAction::CrossSigningReset));
345            assert!(metadata
346                .account_management_actions_supported
347                .contains(&AccountManagementAction::from("custom")));
348        }
349
350        #[cfg(feature = "unstable-msc4108")]
351        assert_eq!(
352            metadata.device_authorization_endpoint.as_ref().map(Url::as_str),
353            Some("https://server.local/device")
354        );
355    }
356
357    #[test]
358    fn metadata_missing_required_fields() {
359        let original_metadata_object = authorization_server_metadata_object();
360
361        let mut metadata_object = original_metadata_object.clone();
362        assert!(metadata_object.remove("issuer").is_some());
363        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
364
365        let mut metadata_object = original_metadata_object.clone();
366        assert!(metadata_object.remove("authorization_endpoint").is_some());
367        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
368
369        let mut metadata_object = original_metadata_object.clone();
370        assert!(metadata_object.remove("token_endpoint").is_some());
371        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
372
373        let mut metadata_object = original_metadata_object.clone();
374        assert!(metadata_object.remove("response_types_supported").is_some());
375        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
376
377        let mut metadata_object = original_metadata_object.clone();
378        assert!(metadata_object.remove("grant_types_supported").is_some());
379        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
380
381        let mut metadata_object = original_metadata_object.clone();
382        assert!(metadata_object.remove("revocation_endpoint").is_some());
383        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
384
385        let mut metadata_object = original_metadata_object;
386        assert!(metadata_object.remove("code_challenge_methods_supported").is_some());
387        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
388    }
389
390    #[test]
391    fn metadata_missing_required_values() {
392        let original_metadata_object = authorization_server_metadata_object();
393
394        let mut metadata_object = original_metadata_object.clone();
395        get_mut_array(&mut metadata_object, "response_types_supported").clear();
396        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
397
398        let mut metadata_object = original_metadata_object.clone();
399        get_mut_array(&mut metadata_object, "response_modes_supported").clear();
400        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
401
402        let mut metadata_object = original_metadata_object.clone();
403        get_mut_array(&mut metadata_object, "response_modes_supported").remove(0);
404        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
405
406        let mut metadata_object = original_metadata_object.clone();
407        get_mut_array(&mut metadata_object, "response_modes_supported").remove(1);
408        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
409
410        let mut metadata_object = original_metadata_object.clone();
411        get_mut_array(&mut metadata_object, "grant_types_supported").clear();
412        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
413
414        let mut metadata_object = original_metadata_object.clone();
415        get_mut_array(&mut metadata_object, "grant_types_supported").remove(0);
416        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
417
418        let mut metadata_object = original_metadata_object.clone();
419        get_mut_array(&mut metadata_object, "grant_types_supported").remove(1);
420        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
421
422        let mut metadata_object = original_metadata_object;
423        get_mut_array(&mut metadata_object, "code_challenge_methods_supported").clear();
424        from_json_value::<AuthorizationServerMetadata>(metadata_object.into()).unwrap_err();
425    }
426}