1mod serde;
6
7pub mod v1 {
8 use std::collections::BTreeSet;
13
14 use ruma_common::{
15 DeviceId,
16 api::{auth_scheme::NoAccessToken, request, response},
17 metadata,
18 serde::{Raw, StringEnum},
19 };
20 use serde::Serialize;
21 use url::Url;
22
23 use crate::PrivOwnedStr;
24
25 metadata! {
26 method: GET,
27 rate_limited: false,
28 authentication: NoAccessToken,
29 history: {
30 unstable => "/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
31 1.15 => "/_matrix/client/v1/auth_metadata",
32 }
33 }
34
35 #[request]
37 #[derive(Default)]
38 pub struct Request {}
39
40 #[response]
42 pub struct Response {
43 #[ruma_api(body)]
47 pub metadata: Raw<AuthorizationServerMetadata>,
48 }
49
50 impl Request {
51 pub fn new() -> Self {
53 Self {}
54 }
55 }
56
57 impl Response {
58 pub fn new(metadata: Raw<AuthorizationServerMetadata>) -> Self {
60 Self { metadata }
61 }
62 }
63
64 #[derive(Debug, Clone, Serialize)]
80 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
81 pub struct AuthorizationServerMetadata {
82 pub issuer: Url,
86
87 pub authorization_endpoint: Url,
91
92 pub token_endpoint: Url,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
102 pub registration_endpoint: Option<Url>,
103
104 pub response_types_supported: BTreeSet<ResponseType>,
113
114 pub response_modes_supported: BTreeSet<ResponseMode>,
122
123 pub grant_types_supported: BTreeSet<GrantType>,
133
134 pub revocation_endpoint: Url,
138
139 pub code_challenge_methods_supported: BTreeSet<CodeChallengeMethod>,
146
147 #[serde(skip_serializing_if = "Option::is_none")]
152 pub account_management_uri: Option<Url>,
153
154 #[serde(skip_serializing_if = "BTreeSet::is_empty")]
158 pub account_management_actions_supported: BTreeSet<AccountManagementAction>,
159
160 #[serde(skip_serializing_if = "Option::is_none")]
164 pub device_authorization_endpoint: Option<Url>,
165
166 #[serde(skip_serializing_if = "Vec::is_empty")]
171 pub prompt_values_supported: Vec<Prompt>,
172 }
173
174 impl AuthorizationServerMetadata {
175 pub fn validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
183 self.validate_urls_inner(false)
184 }
185
186 pub fn insecure_validate_urls(&self) -> Result<(), AuthorizationServerMetadataUrlError> {
194 self.validate_urls_inner(true)
195 }
196
197 fn validate_urls_inner(
200 &self,
201 insecure: bool,
202 ) -> Result<(), AuthorizationServerMetadataUrlError> {
203 if self.issuer.query().is_some() || self.issuer.fragment().is_some() {
204 return Err(AuthorizationServerMetadataUrlError::IssuerHasQueryOrFragment);
205 }
206
207 if insecure {
208 return Ok(());
210 }
211
212 let required_urls = &[
213 ("issuer", &self.issuer),
214 ("authorization_endpoint", &self.authorization_endpoint),
215 ("token_endpoint", &self.token_endpoint),
216 ("revocation_endpoint", &self.revocation_endpoint),
217 ];
218 let optional_urls = &[
219 self.registration_endpoint.as_ref().map(|string| ("registration_endpoint", string)),
220 self.account_management_uri
221 .as_ref()
222 .map(|string| ("account_management_uri", string)),
223 self.device_authorization_endpoint
224 .as_ref()
225 .map(|string| ("device_authorization_endpoint", string)),
226 ];
227
228 for (field, url) in required_urls.iter().chain(optional_urls.iter().flatten()) {
229 if url.scheme() != "https" {
230 return Err(AuthorizationServerMetadataUrlError::NotHttpsScheme(field));
231 }
232 }
233
234 Ok(())
235 }
236
237 pub fn is_account_management_action_supported(
242 &self,
243 action: &AccountManagementAction,
244 ) -> bool {
245 match action {
246 AccountManagementAction::UnstableSessionsList
247 | AccountManagementAction::DevicesList => {
248 self.account_management_actions_supported
249 .contains(&AccountManagementAction::DevicesList)
250 || self
251 .account_management_actions_supported
252 .contains(&AccountManagementAction::UnstableSessionsList)
253 }
254 AccountManagementAction::UnstableSessionView
255 | AccountManagementAction::DeviceView => {
256 self.account_management_actions_supported
257 .contains(&AccountManagementAction::DeviceView)
258 || self
259 .account_management_actions_supported
260 .contains(&AccountManagementAction::UnstableSessionView)
261 }
262 AccountManagementAction::UnstableSessionEnd
263 | AccountManagementAction::DeviceDelete => {
264 self.account_management_actions_supported
265 .contains(&AccountManagementAction::DeviceDelete)
266 || self
267 .account_management_actions_supported
268 .contains(&AccountManagementAction::UnstableSessionEnd)
269 }
270 action => self.account_management_actions_supported.contains(action),
271 }
272 }
273
274 pub fn account_management_url_with_action(
283 &self,
284 action: AccountManagementActionData<'_>,
285 ) -> Option<Url> {
286 const QUERY_NAME_ACTION: &str = "action";
287 const QUERY_NAME_DEVICE_ID: &str = "device_id";
288
289 let mut url = self.account_management_uri.clone()?;
290
291 match action {
292 AccountManagementActionData::Profile => {
293 url.query_pairs_mut()
294 .append_pair(QUERY_NAME_ACTION, AccountManagementAction::Profile.as_str());
295 }
296 AccountManagementActionData::DevicesList => {
297 let action = if self
300 .account_management_actions_supported
301 .contains(&AccountManagementAction::DevicesList)
302 {
303 AccountManagementAction::DevicesList
304 } else if self
305 .account_management_actions_supported
306 .contains(&AccountManagementAction::UnstableSessionsList)
307 {
308 AccountManagementAction::UnstableSessionsList
309 } else {
310 AccountManagementAction::DevicesList
311 };
312
313 url.query_pairs_mut().append_pair(QUERY_NAME_ACTION, action.as_str());
314 }
315 AccountManagementActionData::DeviceView(DeviceViewData { device_id }) => {
316 let action = if self
319 .account_management_actions_supported
320 .contains(&AccountManagementAction::DeviceView)
321 {
322 AccountManagementAction::DeviceView
323 } else if self
324 .account_management_actions_supported
325 .contains(&AccountManagementAction::UnstableSessionView)
326 {
327 AccountManagementAction::UnstableSessionView
328 } else {
329 AccountManagementAction::DeviceView
330 };
331
332 url.query_pairs_mut()
333 .append_pair(QUERY_NAME_ACTION, action.as_str())
334 .append_pair(QUERY_NAME_DEVICE_ID, device_id.as_str());
335 }
336 AccountManagementActionData::DeviceDelete(DeviceDeleteData { device_id }) => {
337 let action = if self
340 .account_management_actions_supported
341 .contains(&AccountManagementAction::DeviceDelete)
342 {
343 AccountManagementAction::DeviceDelete
344 } else if self
345 .account_management_actions_supported
346 .contains(&AccountManagementAction::UnstableSessionEnd)
347 {
348 AccountManagementAction::UnstableSessionEnd
349 } else {
350 AccountManagementAction::DeviceDelete
351 };
352
353 url.query_pairs_mut()
354 .append_pair(QUERY_NAME_ACTION, action.as_str())
355 .append_pair(QUERY_NAME_DEVICE_ID, device_id.as_str());
356 }
357 AccountManagementActionData::AccountDeactivate => {
358 url.query_pairs_mut().append_pair(
359 QUERY_NAME_ACTION,
360 AccountManagementAction::AccountDeactivate.as_str(),
361 );
362 }
363 AccountManagementActionData::CrossSigningReset => {
364 url.query_pairs_mut().append_pair(
365 QUERY_NAME_ACTION,
366 AccountManagementAction::CrossSigningReset.as_str(),
367 );
368 }
369 }
370
371 Some(url)
372 }
373 }
374
375 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
377 #[derive(Clone, StringEnum)]
378 #[ruma_enum(rename_all = "lowercase")]
379 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
380 pub enum ResponseType {
381 Code,
385
386 #[doc(hidden)]
387 _Custom(PrivOwnedStr),
388 }
389
390 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
395 #[derive(Clone, StringEnum)]
398 #[ruma_enum(rename_all = "lowercase")]
399 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
400 pub enum ResponseMode {
401 Query,
404
405 Fragment,
408
409 #[doc(hidden)]
410 _Custom(PrivOwnedStr),
411 }
412
413 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
415 #[derive(Clone, StringEnum)]
416 #[ruma_enum(rename_all = "snake_case")]
417 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
418 pub enum GrantType {
419 AuthorizationCode,
423
424 RefreshToken,
428
429 #[ruma_enum(rename = "urn:ietf:params:oauth:grant-type:device_code")]
433 DeviceCode,
434
435 #[doc(hidden)]
436 _Custom(PrivOwnedStr),
437 }
438
439 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
441 #[derive(Clone, StringEnum)]
442 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
443 pub enum CodeChallengeMethod {
444 S256,
448
449 #[doc(hidden)]
450 _Custom(PrivOwnedStr),
451 }
452
453 #[doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/doc/string_enum.md"))]
459 #[derive(Clone, StringEnum)]
463 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
464 pub enum AccountManagementAction {
465 #[ruma_enum(rename = "org.matrix.profile")]
467 Profile,
468
469 #[ruma_enum(rename = "org.matrix.sessions_list")]
476 UnstableSessionsList,
477
478 #[ruma_enum(rename = "org.matrix.devices_list")]
480 DevicesList,
481
482 #[ruma_enum(rename = "org.matrix.session_view")]
489 UnstableSessionView,
490
491 #[ruma_enum(rename = "org.matrix.device_view")]
493 DeviceView,
494
495 #[ruma_enum(rename = "org.matrix.session_end")]
502 UnstableSessionEnd,
503
504 #[ruma_enum(rename = "org.matrix.device_delete")]
506 DeviceDelete,
507
508 #[ruma_enum(rename = "org.matrix.account_deactivate")]
510 AccountDeactivate,
511
512 #[ruma_enum(rename = "org.matrix.cross_signing_reset")]
518 CrossSigningReset,
519
520 #[doc(hidden)]
521 _Custom(PrivOwnedStr),
522 }
523
524 #[derive(Debug, Clone)]
529 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
530 pub enum AccountManagementActionData<'a> {
531 Profile,
533
534 DevicesList,
536
537 DeviceView(DeviceViewData<'a>),
539
540 DeviceDelete(DeviceDeleteData<'a>),
542
543 AccountDeactivate,
545
546 CrossSigningReset,
548 }
549
550 #[derive(Debug, Clone)]
552 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
553 pub struct DeviceViewData<'a> {
554 pub device_id: &'a DeviceId,
556 }
557
558 impl<'a> DeviceViewData<'a> {
559 pub fn new(device_id: &'a DeviceId) -> Self {
561 Self { device_id }
562 }
563 }
564
565 impl<'a> From<&'a DeviceId> for DeviceViewData<'a> {
566 fn from(device_id: &'a DeviceId) -> Self {
567 Self::new(device_id)
568 }
569 }
570
571 #[derive(Debug, Clone)]
573 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
574 pub struct DeviceDeleteData<'a> {
575 pub device_id: &'a DeviceId,
577 }
578
579 impl<'a> DeviceDeleteData<'a> {
580 pub fn new(device_id: &'a DeviceId) -> Self {
582 Self { device_id }
583 }
584 }
585
586 impl<'a> From<&'a DeviceId> for DeviceDeleteData<'a> {
587 fn from(device_id: &'a DeviceId) -> Self {
588 Self::new(device_id)
589 }
590 }
591
592 #[derive(Debug, Clone, thiserror::Error)]
594 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
595 pub enum AuthorizationServerMetadataUrlError {
596 #[error("URL in `{0}` must use the `https` scheme")]
598 NotHttpsScheme(&'static str),
599
600 #[error("URL in `issuer` cannot have a query or fragment component")]
602 IssuerHasQueryOrFragment,
603 }
604
605 #[derive(Clone, StringEnum)]
607 #[ruma_enum(rename_all = "lowercase")]
608 #[cfg_attr(not(ruma_unstable_exhaustive_types), non_exhaustive)]
609 pub enum Prompt {
610 Create,
615
616 #[doc(hidden)]
617 _Custom(PrivOwnedStr),
618 }
619}
620
621#[cfg(test)]
622mod tests {
623 use ruma_common::device_id;
624 use serde_json::{Value as JsonValue, from_value as from_json_value, json};
625 use url::Url;
626
627 use super::v1::{
628 AccountManagementAction, AccountManagementActionData, AuthorizationServerMetadata,
629 };
630
631 pub(super) fn authorization_server_metadata_json() -> JsonValue {
633 json!({
634 "issuer": "https://server.local/",
635 "authorization_endpoint": "https://server.local/authorize",
636 "token_endpoint": "https://server.local/token",
637 "registration_endpoint": "https://server.local/register",
638 "response_types_supported": ["code"],
639 "response_modes_supported": ["query", "fragment"],
640 "grant_types_supported": ["authorization_code", "refresh_token"],
641 "revocation_endpoint": "https://server.local/revoke",
642 "code_challenge_methods_supported": ["S256"],
643 "account_management_uri": "https://server.local/account",
644 "account_management_actions_supported": [
645 "org.matrix.profile",
646 "org.matrix.devices_list",
647 "org.matrix.device_view",
648 "org.matrix.device_delete",
649 "org.matrix.account_deactivate",
650 "org.matrix.cross_signing_reset",
651 ],
652 "device_authorization_endpoint": "https://server.local/device",
653 })
654 }
655
656 fn authorization_server_metadata() -> AuthorizationServerMetadata {
658 from_json_value(authorization_server_metadata_json()).unwrap()
659 }
660
661 #[test]
662 fn metadata_valid_urls() {
663 let metadata = authorization_server_metadata();
664 metadata.validate_urls().unwrap();
665 metadata.insecure_validate_urls().unwrap();
666 }
667
668 #[test]
669 fn metadata_invalid_or_insecure_issuer() {
670 let original_metadata = authorization_server_metadata();
671
672 let mut metadata = original_metadata.clone();
674 metadata.issuer = Url::parse("https://server.local/?session=1er45elp").unwrap();
675 metadata.validate_urls().unwrap_err();
676 metadata.insecure_validate_urls().unwrap_err();
677
678 let mut metadata = original_metadata.clone();
680 metadata.issuer = Url::parse("https://server.local/#session").unwrap();
681 metadata.validate_urls().unwrap_err();
682 metadata.insecure_validate_urls().unwrap_err();
683
684 let mut metadata = original_metadata;
686 metadata.issuer = Url::parse("http://server.local/").unwrap();
687 metadata.validate_urls().unwrap_err();
688 metadata.insecure_validate_urls().unwrap();
689 }
690
691 #[test]
692 fn metadata_insecure_urls() {
693 let original_metadata = authorization_server_metadata();
694
695 let mut metadata = original_metadata.clone();
696 metadata.authorization_endpoint = Url::parse("http://server.local/authorize").unwrap();
697 metadata.validate_urls().unwrap_err();
698 metadata.insecure_validate_urls().unwrap();
699
700 let mut metadata = original_metadata.clone();
701 metadata.token_endpoint = Url::parse("http://server.local/token").unwrap();
702 metadata.validate_urls().unwrap_err();
703 metadata.insecure_validate_urls().unwrap();
704
705 let mut metadata = original_metadata.clone();
706 metadata.registration_endpoint = Some(Url::parse("http://server.local/register").unwrap());
707 metadata.validate_urls().unwrap_err();
708 metadata.insecure_validate_urls().unwrap();
709
710 let mut metadata = original_metadata.clone();
711 metadata.revocation_endpoint = Url::parse("http://server.local/revoke").unwrap();
712 metadata.validate_urls().unwrap_err();
713 metadata.insecure_validate_urls().unwrap();
714
715 let mut metadata = original_metadata.clone();
716 metadata.account_management_uri = Some(Url::parse("http://server.local/account").unwrap());
717 metadata.validate_urls().unwrap_err();
718 metadata.insecure_validate_urls().unwrap();
719
720 let mut metadata = original_metadata;
721 metadata.device_authorization_endpoint =
722 Some(Url::parse("http://server.local/device").unwrap());
723 metadata.validate_urls().unwrap_err();
724 metadata.insecure_validate_urls().unwrap();
725 }
726
727 #[test]
728 fn metadata_is_account_management_action_supported() {
729 let mut original_metadata = authorization_server_metadata();
730
731 assert!(
733 original_metadata
734 .is_account_management_action_supported(&AccountManagementAction::Profile)
735 );
736
737 original_metadata
739 .account_management_actions_supported
740 .remove(&AccountManagementAction::Profile);
741 assert!(
742 !original_metadata
743 .is_account_management_action_supported(&AccountManagementAction::Profile)
744 );
745
746 assert!(
748 original_metadata
749 .is_account_management_action_supported(&AccountManagementAction::DevicesList)
750 );
751 assert!(original_metadata.is_account_management_action_supported(
752 &AccountManagementAction::UnstableSessionsList
753 ));
754
755 original_metadata
757 .account_management_actions_supported
758 .remove(&AccountManagementAction::DevicesList);
759 assert!(
760 !original_metadata
761 .is_account_management_action_supported(&AccountManagementAction::DevicesList)
762 );
763 assert!(!original_metadata.is_account_management_action_supported(
764 &AccountManagementAction::UnstableSessionsList
765 ));
766
767 assert!(
769 original_metadata
770 .is_account_management_action_supported(&AccountManagementAction::DeviceView)
771 );
772 assert!(
773 original_metadata.is_account_management_action_supported(
774 &AccountManagementAction::UnstableSessionView
775 )
776 );
777
778 original_metadata
780 .account_management_actions_supported
781 .remove(&AccountManagementAction::DeviceView);
782 assert!(
783 !original_metadata
784 .is_account_management_action_supported(&AccountManagementAction::DeviceView)
785 );
786 assert!(
787 !original_metadata.is_account_management_action_supported(
788 &AccountManagementAction::UnstableSessionView
789 )
790 );
791
792 assert!(
794 original_metadata
795 .is_account_management_action_supported(&AccountManagementAction::DeviceDelete)
796 );
797 assert!(
798 original_metadata.is_account_management_action_supported(
799 &AccountManagementAction::UnstableSessionEnd
800 )
801 );
802
803 original_metadata
805 .account_management_actions_supported
806 .remove(&AccountManagementAction::DeviceDelete);
807 assert!(
808 !original_metadata
809 .is_account_management_action_supported(&AccountManagementAction::DeviceDelete)
810 );
811 assert!(
812 !original_metadata.is_account_management_action_supported(
813 &AccountManagementAction::UnstableSessionEnd
814 )
815 );
816 }
817
818 #[test]
819 fn metadata_account_management_url_with_action() {
820 let mut original_metadata = authorization_server_metadata();
821 let device_id = device_id!("DEVICE");
822
823 let url = original_metadata
825 .account_management_url_with_action(AccountManagementActionData::Profile)
826 .unwrap();
827 assert_eq!(url.query().unwrap(), "action=org.matrix.profile");
828
829 let url = original_metadata
831 .account_management_url_with_action(AccountManagementActionData::DevicesList)
832 .unwrap();
833 assert_eq!(url.query().unwrap(), "action=org.matrix.devices_list");
834
835 original_metadata
837 .account_management_actions_supported
838 .insert(AccountManagementAction::UnstableSessionsList);
839 let url = original_metadata
840 .account_management_url_with_action(AccountManagementActionData::DevicesList)
841 .unwrap();
842 assert_eq!(url.query().unwrap(), "action=org.matrix.devices_list");
843
844 original_metadata
846 .account_management_actions_supported
847 .remove(&AccountManagementAction::DevicesList);
848 let url = original_metadata
849 .account_management_url_with_action(AccountManagementActionData::DevicesList)
850 .unwrap();
851 assert_eq!(url.query().unwrap(), "action=org.matrix.sessions_list");
852
853 original_metadata
855 .account_management_actions_supported
856 .remove(&AccountManagementAction::UnstableSessionsList);
857 let url = original_metadata
858 .account_management_url_with_action(AccountManagementActionData::DevicesList)
859 .unwrap();
860 assert_eq!(url.query().unwrap(), "action=org.matrix.devices_list");
861
862 let url = original_metadata
864 .account_management_url_with_action(AccountManagementActionData::DeviceView(
865 device_id.into(),
866 ))
867 .unwrap();
868 assert_eq!(url.query().unwrap(), "action=org.matrix.device_view&device_id=DEVICE");
869
870 original_metadata
872 .account_management_actions_supported
873 .insert(AccountManagementAction::UnstableSessionView);
874 let url = original_metadata
875 .account_management_url_with_action(AccountManagementActionData::DeviceView(
876 device_id.into(),
877 ))
878 .unwrap();
879 assert_eq!(url.query().unwrap(), "action=org.matrix.device_view&device_id=DEVICE");
880
881 original_metadata
883 .account_management_actions_supported
884 .remove(&AccountManagementAction::DeviceView);
885 let url = original_metadata
886 .account_management_url_with_action(AccountManagementActionData::DeviceView(
887 device_id.into(),
888 ))
889 .unwrap();
890 assert_eq!(url.query().unwrap(), "action=org.matrix.session_view&device_id=DEVICE");
891
892 original_metadata
894 .account_management_actions_supported
895 .remove(&AccountManagementAction::UnstableSessionView);
896 let url = original_metadata
897 .account_management_url_with_action(AccountManagementActionData::DeviceView(
898 device_id.into(),
899 ))
900 .unwrap();
901 assert_eq!(url.query().unwrap(), "action=org.matrix.device_view&device_id=DEVICE");
902
903 let url = original_metadata
905 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
906 device_id.into(),
907 ))
908 .unwrap();
909 assert_eq!(url.query().unwrap(), "action=org.matrix.device_delete&device_id=DEVICE");
910
911 original_metadata
913 .account_management_actions_supported
914 .insert(AccountManagementAction::UnstableSessionEnd);
915 let url = original_metadata
916 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
917 device_id.into(),
918 ))
919 .unwrap();
920 assert_eq!(url.query().unwrap(), "action=org.matrix.device_delete&device_id=DEVICE");
921
922 original_metadata
924 .account_management_actions_supported
925 .remove(&AccountManagementAction::DeviceDelete);
926 let url = original_metadata
927 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
928 device_id.into(),
929 ))
930 .unwrap();
931 assert_eq!(url.query().unwrap(), "action=org.matrix.session_end&device_id=DEVICE");
932
933 original_metadata
935 .account_management_actions_supported
936 .remove(&AccountManagementAction::UnstableSessionEnd);
937 let url = original_metadata
938 .account_management_url_with_action(AccountManagementActionData::DeviceDelete(
939 device_id.into(),
940 ))
941 .unwrap();
942 assert_eq!(url.query().unwrap(), "action=org.matrix.device_delete&device_id=DEVICE");
943 }
944}