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