ruma_client_api/discovery/
get_authorization_server_metadata.rs

1//! `GET /_matrix/client/*/auth_metadata`
2//!
3//! Get the metadata of the authorization server that is trusted by the homeserver.
4
5mod serde;
6
7pub mod v1 {
8    //! `v1` ([spec])
9    //!
10    //! [spec]: https://spec.matrix.org/latest/client-server-api/#get_matrixclientv1auth_metadata
11
12    use std::collections::BTreeSet;
13
14    use ruma_common::{
15        api::{request, response, Metadata},
16        metadata,
17        serde::{OrdAsRefStr, PartialEqAsRefStr, PartialOrdAsRefStr, Raw, StringEnum},
18    };
19    use serde::Serialize;
20    use url::Url;
21
22    use crate::PrivOwnedStr;
23
24    const METADATA: Metadata = metadata! {
25        method: GET,
26        rate_limited: false,
27        authentication: None,
28        history: {
29            unstable => "/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
30            1.15 => "/_matrix/client/v1/auth_metadata",
31        }
32    };
33
34    /// Request type for the `auth_metadata` endpoint.
35    #[request(error = crate::Error)]
36    #[derive(Default)]
37    pub struct Request {}
38
39    /// Request type for the `auth_metadata` endpoint.
40    #[response(error = crate::Error)]
41    pub struct Response {
42        /// The authorization server metadata as defined in [RFC 8414].
43        ///
44        /// [RFC 8414]: https://datatracker.ietf.org/doc/html/rfc8414
45        #[ruma_api(body)]
46        pub metadata: Raw<AuthorizationServerMetadata>,
47    }
48
49    impl Request {
50        /// Creates a new empty `Request`.
51        pub fn new() -> Self {
52            Self {}
53        }
54    }
55
56    impl Response {
57        /// Creates a new `Response` with the given serialized authorization server metadata.
58        pub fn new(metadata: Raw<AuthorizationServerMetadata>) -> Self {
59            Self { metadata }
60        }
61    }
62
63    /// Metadata describing the configuration of the authorization server.
64    ///
65    /// While the metadata properties and their values are declared for OAuth 2.0 in [RFC 8414] and
66    /// other RFCs, this type only supports properties and values that are used for Matrix, as
67    /// specified in [MSC3861] and its dependencies.
68    ///
69    /// This type is validated to have at least all the required values during deserialization. The
70    /// URLs are not validated during deserialization, to validate them use
71    /// [`AuthorizationServerMetadata::validate_urls()`] or
72    /// [`AuthorizationServerMetadata::insecure_validate_urls()`].
73    ///
74    /// This type has no constructor, it should be sent as raw JSON directly.
75    ///
76    /// [RFC 8414]: https://datatracker.ietf.org/doc/html/rfc8414
77    /// [MSC3861]: https://github.com/matrix-org/matrix-spec-proposals/pull/3861
78    #[derive(Debug, Clone, Serialize)]
79    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
80    pub struct AuthorizationServerMetadata {
81        /// The authorization server's issuer identifier.
82        ///
83        /// This should be a URL with no query or fragment components.
84        pub issuer: Url,
85
86        /// URL of the authorization server's authorization endpoint ([RFC 6749]).
87        ///
88        /// [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
89        pub authorization_endpoint: Url,
90
91        /// URL of the authorization server's token endpoint ([RFC 6749]).
92        ///
93        /// [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
94        pub token_endpoint: Url,
95
96        /// URL of the authorization server's OAuth 2.0 Dynamic Client Registration endpoint
97        /// ([RFC 7591]).
98        ///
99        /// [RFC 7591]: https://datatracker.ietf.org/doc/html/rfc7591
100        #[serde(skip_serializing_if = "Option::is_none")]
101        pub registration_endpoint: Option<Url>,
102
103        /// List of the OAuth 2.0 `response_type` values that this authorization server supports.
104        ///
105        /// Those values are the same as those used with the `response_types` parameter defined by
106        /// OAuth 2.0 Dynamic Client Registration ([RFC 7591]).
107        ///
108        /// This field must include [`ResponseType::Code`].
109        ///
110        /// [RFC 7591]: https://datatracker.ietf.org/doc/html/rfc7591
111        pub response_types_supported: BTreeSet<ResponseType>,
112
113        /// List of the OAuth 2.0 `response_mode` values that this authorization server supports.
114        ///
115        /// Those values are specified in [OAuth 2.0 Multiple Response Type Encoding Practices].
116        ///
117        /// This field must include [`ResponseMode::Query`] and [`ResponseMode::Fragment`].
118        ///
119        /// [OAuth 2.0 Multiple Response Type Encoding Practices]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
120        pub response_modes_supported: BTreeSet<ResponseMode>,
121
122        /// List of the OAuth 2.0 `grant_type` values that this authorization server supports.
123        ///
124        /// Those values are the same as those used with the `grant_types` parameter defined by
125        /// OAuth 2.0 Dynamic Client Registration ([RFC 7591]).
126        ///
127        /// This field must include [`GrantType::AuthorizationCode`] and
128        /// [`GrantType::RefreshToken`].
129        ///
130        /// [RFC 7591]: https://datatracker.ietf.org/doc/html/rfc7591
131        pub grant_types_supported: BTreeSet<GrantType>,
132
133        /// URL of the authorization server's OAuth 2.0 revocation endpoint ([RFC 7009]).
134        ///
135        /// [RFC 7009]: https://datatracker.ietf.org/doc/html/rfc7009
136        pub revocation_endpoint: Url,
137
138        /// List of Proof Key for Code Exchange (PKCE) code challenge methods supported by this
139        /// authorization server ([RFC 7636]).
140        ///
141        /// This field must include [`CodeChallengeMethod::S256`].
142        ///
143        /// [RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636
144        pub code_challenge_methods_supported: BTreeSet<CodeChallengeMethod>,
145
146        /// URL where the user is able to access the account management capabilities of the
147        /// authorization server ([MSC4191]).
148        ///
149        /// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
150        #[cfg(feature = "unstable-msc4191")]
151        #[serde(skip_serializing_if = "Option::is_none")]
152        pub account_management_uri: Option<Url>,
153
154        /// List of actions that the account management URL supports ([MSC4191]).
155        ///
156        /// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
157        #[cfg(feature = "unstable-msc4191")]
158        #[serde(skip_serializing_if = "BTreeSet::is_empty")]
159        pub account_management_actions_supported: BTreeSet<AccountManagementAction>,
160
161        /// URL of the authorization server's device authorization endpoint ([RFC 8628]).
162        ///
163        /// [RFC 8628]: https://datatracker.ietf.org/doc/html/rfc8628
164        #[cfg(feature = "unstable-msc4108")]
165        #[serde(skip_serializing_if = "Option::is_none")]
166        pub device_authorization_endpoint: Option<Url>,
167
168        /// The [`Prompt`] values supported by the authorization server ([Initiating User
169        /// Registration via OpenID Connect 1.0]).
170        ///
171        /// [Initiating User Registration via OpenID Connect 1.0]: https://openid.net/specs/openid-connect-prompt-create-1_0.html
172        #[serde(skip_serializing_if = "Vec::is_empty")]
173        pub prompt_values_supported: Vec<Prompt>,
174    }
175
176    impl AuthorizationServerMetadata {
177        /// Strict validation of the URLs in this `AuthorizationServerMetadata`.
178        ///
179        /// This checks that:
180        ///
181        /// * The `issuer` is a valid URL using an `https` scheme and without a query or fragment.
182        ///
183        /// * All the URLs use an `https` scheme.
184        pub fn validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
185            self.validate_urls_inner(false)
186        }
187
188        /// Weak validation the URLs `AuthorizationServerMetadata` are all absolute URLs.
189        ///
190        /// This only checks that the `issuer` is a valid URL without a query or fragment.
191        ///
192        /// In production, you should prefer [`AuthorizationServerMetadata`] that also check if the
193        /// URLs use an `https` scheme. This method is meant for development purposes, when
194        /// interacting with a local authorization server.
195        pub fn insecure_validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
196            self.validate_urls_inner(true)
197        }
198
199        /// Get an iterator over the URLs of this `AuthorizationServerMetadata`, except the
200        /// `issuer`.
201        fn validate_urls_inner(
202            &self,
203            insecure: bool,
204        ) -> Result<(), AuthorizationServerMetadataUrlError> {
205            if self.issuer.query().is_some() || self.issuer.fragment().is_some() {
206                return Err(AuthorizationServerMetadataUrlError::IssuerHasQueryOrFragment);
207            }
208
209            if insecure {
210                // No more checks.
211                return Ok(());
212            }
213
214            let required_urls = &[
215                ("issuer", &self.issuer),
216                ("authorization_endpoint", &self.authorization_endpoint),
217                ("token_endpoint", &self.token_endpoint),
218                ("revocation_endpoint", &self.revocation_endpoint),
219            ];
220            let optional_urls = &[
221                self.registration_endpoint.as_ref().map(|string| ("registration_endpoint", string)),
222                #[cfg(feature = "unstable-msc4191")]
223                self.account_management_uri
224                    .as_ref()
225                    .map(|string| ("account_management_uri", string)),
226                #[cfg(feature = "unstable-msc4108")]
227                self.device_authorization_endpoint
228                    .as_ref()
229                    .map(|string| ("device_authorization_endpoint", string)),
230            ];
231
232            for (field, url) in required_urls.iter().chain(optional_urls.iter().flatten()) {
233                if url.scheme() != "https" {
234                    return Err(AuthorizationServerMetadataUrlError::NotHttpsScheme(field));
235                }
236            }
237
238            Ok(())
239        }
240    }
241
242    /// The method to use at the authorization endpoint.
243    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
244    #[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
245    #[ruma_enum(rename_all = "lowercase")]
246    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
247    pub enum ResponseType {
248        /// Use the authorization code grant flow ([RFC 6749]).
249        ///
250        /// [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
251        Code,
252
253        #[doc(hidden)]
254        _Custom(PrivOwnedStr),
255    }
256
257    /// The mechanism to be used for returning authorization response parameters from the
258    /// authorization endpoint.
259    ///
260    /// The values are specified in [OAuth 2.0 Multiple Response Type Encoding Practices].
261    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
262    ///
263    /// [OAuth 2.0 Multiple Response Type Encoding Practices]: https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html
264    #[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
265    #[ruma_enum(rename_all = "lowercase")]
266    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
267    pub enum ResponseMode {
268        /// Authorization Response parameters are encoded in the fragment added to the
269        /// `redirect_uri` when redirecting back to the client.
270        Query,
271
272        /// Authorization Response parameters are encoded in the query string added to the
273        /// `redirect_uri` when redirecting back to the client.
274        Fragment,
275
276        #[doc(hidden)]
277        _Custom(PrivOwnedStr),
278    }
279
280    /// The grant type to use at the token endpoint.
281    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
282    #[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
283    #[ruma_enum(rename_all = "snake_case")]
284    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
285    pub enum GrantType {
286        /// The authorization code grant type ([RFC 6749]).
287        ///
288        /// [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
289        AuthorizationCode,
290
291        /// The refresh token grant type ([RFC 6749]).
292        ///
293        /// [RFC 6749]: https://datatracker.ietf.org/doc/html/rfc6749
294        RefreshToken,
295
296        /// The device code grant type ([RFC 8628]).
297        ///
298        /// [RFC 8628]: https://datatracker.ietf.org/doc/html/rfc8628
299        #[cfg(feature = "unstable-msc4108")]
300        #[ruma_enum(rename = "urn:ietf:params:oauth:grant-type:device_code")]
301        DeviceCode,
302
303        #[doc(hidden)]
304        _Custom(PrivOwnedStr),
305    }
306
307    /// The code challenge method to use at the authorization endpoint.
308    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
309    #[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
310    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
311    pub enum CodeChallengeMethod {
312        /// Use a SHA-256, base64url-encoded code challenge ([RFC 7636]).
313        ///
314        /// [RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636
315        S256,
316
317        #[doc(hidden)]
318        _Custom(PrivOwnedStr),
319    }
320
321    /// The action that the user wishes to do at the account management URL.
322    ///
323    /// The values are specified in [MSC 4191].
324    #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
325    ///
326    /// [MSC 4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191
327    #[cfg(feature = "unstable-msc4191")]
328    #[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
329    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
330    pub enum AccountManagementAction {
331        /// The user wishes to view their profile (name, avatar, contact details).
332        ///
333        /// [RFC 7636]: https://datatracker.ietf.org/doc/html/rfc7636
334        #[ruma_enum(rename = "org.matrix.profile")]
335        Profile,
336
337        /// The user wishes to view a list of their sessions.
338        #[ruma_enum(rename = "org.matrix.sessions_list")]
339        SessionsList,
340
341        /// The user wishes to view the details of a specific session.
342        #[ruma_enum(rename = "org.matrix.session_view")]
343        SessionView,
344
345        /// The user wishes to end/logout a specific session.
346        #[ruma_enum(rename = "org.matrix.session_end")]
347        SessionEnd,
348
349        /// The user wishes to deactivate their account.
350        #[ruma_enum(rename = "org.matrix.account_deactivate")]
351        AccountDeactivate,
352
353        /// The user wishes to reset their cross-signing keys.
354        #[ruma_enum(rename = "org.matrix.cross_signing_reset")]
355        CrossSigningReset,
356
357        #[doc(hidden)]
358        _Custom(PrivOwnedStr),
359    }
360
361    /// The possible errors when validating URLs of [`AuthorizationServerMetadata`].
362    #[derive(Debug, Clone, thiserror::Error)]
363    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
364    pub enum AuthorizationServerMetadataUrlError {
365        /// The URL of the field does not use the `https` scheme.
366        #[error("URL in `{0}` must use the `https` scheme")]
367        NotHttpsScheme(&'static str),
368
369        /// The `issuer` URL has a query or fragment component.
370        #[error("URL in `issuer` cannot have a query or fragment component")]
371        IssuerHasQueryOrFragment,
372    }
373
374    /// The desired user experience when using the authorization endpoint.
375    #[derive(Clone, StringEnum, PartialEqAsRefStr, Eq, PartialOrdAsRefStr, OrdAsRefStr)]
376    #[ruma_enum(rename_all = "lowercase")]
377    #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
378    pub enum Prompt {
379        /// The user wants to create a new account ([Initiating User Registration via OpenID
380        /// Connect 1.0]).
381        ///
382        /// [Initiating User Registration via OpenID Connect 1.0]: https://openid.net/specs/openid-connect-prompt-create-1_0.html
383        Create,
384
385        #[doc(hidden)]
386        _Custom(PrivOwnedStr),
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use serde_json::{from_value as from_json_value, json, Value as JsonValue};
393    use url::Url;
394
395    use super::v1::AuthorizationServerMetadata;
396
397    /// A valid `AuthorizationServerMetadata` with all fields and values, as a JSON object.
398    pub(super) fn authorization_server_metadata_json() -> JsonValue {
399        json!({
400            "issuer": "https://server.local/",
401            "authorization_endpoint": "https://server.local/authorize",
402            "token_endpoint": "https://server.local/token",
403            "registration_endpoint": "https://server.local/register",
404            "response_types_supported": ["code"],
405            "response_modes_supported": ["query", "fragment"],
406            "grant_types_supported": ["authorization_code", "refresh_token"],
407            "revocation_endpoint": "https://server.local/revoke",
408            "code_challenge_methods_supported": ["S256"],
409            "account_management_uri": "https://server.local/account",
410            "account_management_actions_supported": [
411                "org.matrix.profile",
412                "org.matrix.sessions_list",
413                "org.matrix.session_view",
414                "org.matrix.session_end",
415                "org.matrix.account_deactivate",
416                "org.matrix.cross_signing_reset",
417            ],
418            "device_authorization_endpoint": "https://server.local/device",
419        })
420    }
421
422    /// A valid `AuthorizationServerMetadata`, with valid URLs.
423    fn authorization_server_metadata() -> AuthorizationServerMetadata {
424        from_json_value(authorization_server_metadata_json()).unwrap()
425    }
426
427    #[test]
428    fn metadata_valid_urls() {
429        let metadata = authorization_server_metadata();
430        metadata.validate_urls().unwrap();
431        metadata.insecure_validate_urls().unwrap();
432    }
433
434    #[test]
435    fn metadata_invalid_or_insecure_issuer() {
436        let original_metadata = authorization_server_metadata();
437
438        // URL with query string.
439        let mut metadata = original_metadata.clone();
440        metadata.issuer = Url::parse("https://server.local/?session=1er45elp").unwrap();
441        metadata.validate_urls().unwrap_err();
442        metadata.insecure_validate_urls().unwrap_err();
443
444        // URL with fragment.
445        let mut metadata = original_metadata.clone();
446        metadata.issuer = Url::parse("https://server.local/#session").unwrap();
447        metadata.validate_urls().unwrap_err();
448        metadata.insecure_validate_urls().unwrap_err();
449
450        // Insecure URL.
451        let mut metadata = original_metadata;
452        metadata.issuer = Url::parse("http://server.local/").unwrap();
453        metadata.validate_urls().unwrap_err();
454        metadata.insecure_validate_urls().unwrap();
455    }
456
457    #[test]
458    fn metadata_insecure_urls() {
459        let original_metadata = authorization_server_metadata();
460
461        let mut metadata = original_metadata.clone();
462        metadata.authorization_endpoint = Url::parse("http://server.local/authorize").unwrap();
463        metadata.validate_urls().unwrap_err();
464        metadata.insecure_validate_urls().unwrap();
465
466        let mut metadata = original_metadata.clone();
467        metadata.token_endpoint = Url::parse("http://server.local/token").unwrap();
468        metadata.validate_urls().unwrap_err();
469        metadata.insecure_validate_urls().unwrap();
470
471        let mut metadata = original_metadata.clone();
472        metadata.registration_endpoint = Some(Url::parse("http://server.local/register").unwrap());
473        metadata.validate_urls().unwrap_err();
474        metadata.insecure_validate_urls().unwrap();
475
476        let mut metadata = original_metadata.clone();
477        metadata.revocation_endpoint = Url::parse("http://server.local/revoke").unwrap();
478        metadata.validate_urls().unwrap_err();
479        metadata.insecure_validate_urls().unwrap();
480
481        #[cfg(feature = "unstable-msc4191")]
482        {
483            let mut metadata = original_metadata.clone();
484            metadata.account_management_uri =
485                Some(Url::parse("http://server.local/account").unwrap());
486            metadata.validate_urls().unwrap_err();
487            metadata.insecure_validate_urls().unwrap();
488        }
489
490        #[cfg(feature = "unstable-msc4108")]
491        {
492            let mut metadata = original_metadata.clone();
493            metadata.device_authorization_endpoint =
494                Some(Url::parse("http://server.local/device").unwrap());
495            metadata.validate_urls().unwrap_err();
496            metadata.insecure_validate_urls().unwrap();
497        }
498    }
499}