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}