diff --git a/bindings/matrix-sdk-ffi/CHANGELOG.md b/bindings/matrix-sdk-ffi/CHANGELOG.md index 0ed6e6a85..5f7770a36 100644 --- a/bindings/matrix-sdk-ffi/CHANGELOG.md +++ b/bindings/matrix-sdk-ffi/CHANGELOG.md @@ -90,6 +90,12 @@ All notable changes to this project will be documented in this file. ### Refactor +- [**breaking**] The following variants of `AccountManagementAction` were + renamed to match their new names after being merge in the Matrix specification: + - `SessionsList` is renamed to `DevicesList` + - `SessionView` is renamed to `DeviceView` + - `SessionEnd` is renamed to `DeviceDelete` + ([#6217](https://github.com/matrix-org/matrix-rust-sdk/pull/6217)) - [**breaking**] `HumanQrGrantLoginError::UnableToCreateDevice` has been removed ([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141) - [**breaking**] Removed `ClientBuilder::enable_oidc_refresh_lock` in favour of using `ClientBuilder::cross_process_lock_config` diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 91af24f67..5903b0e67 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -27,9 +27,7 @@ use matrix_sdk::media::MediaFileHandle as SdkMediaFileHandle; #[cfg(feature = "sqlite")] use matrix_sdk::STATE_STORE_DATABASE_NAME; use matrix_sdk::{ - authentication::oauth::{ - AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthSession, - }, + authentication::oauth::{ClientId, OAuthAuthorizationData, OAuthError, OAuthSession}, deserialized_responses::RawAnySyncOrStrippedTimelineEvent, executor::AbortOnDrop, media::{MediaFormat, MediaRequestParameters, MediaRetentionPolicy, MediaThumbnailSettings}, @@ -75,6 +73,9 @@ use oauth2::Scope; use ruma::{ api::client::{ alias::get_alias, + discovery::get_authorization_server_metadata::v1::{ + AccountManagementActionData, DeviceDeleteData, DeviceViewData, + }, error::ErrorKind, profile::{AvatarUrl, DisplayName}, room::create_room::{v3::CreationContent, RoomPowerLevelsContentOverride}, @@ -1312,20 +1313,20 @@ impl Client { return Ok(None); } - let mut url_builder = match self.inner.oauth().account_management_url().await { - Ok(Some(url_builder)) => url_builder, - Ok(None) => return Ok(None), + let server_metadata = match self.inner.oauth().cached_server_metadata().await { + Ok(server_metadata) => server_metadata, Err(e) => { - error!("Failed retrieving account management URL: {e}"); - return Err(e.into()); + error!("Failed retrieving cached server metadata: {e}"); + return Err(OAuthError::from(e).into()); } }; - if let Some(action) = action { - url_builder = url_builder.action(action.into()); + Ok(if let Some(action) = &action { + server_metadata.account_management_url_with_action(action.into()) + } else { + server_metadata.account_management_uri } - - Ok(Some(url_builder.build().to_string())) + .map(Into::into)) } pub fn user_id(&self) -> Result { @@ -2661,23 +2662,23 @@ pub(crate) struct OidcSessionData { #[derive(uniffi::Enum)] pub enum AccountManagementAction { Profile, - SessionsList, - SessionView { device_id: String }, - SessionEnd { device_id: String }, + DevicesList, + DeviceView { device_id: String }, + DeviceDelete { device_id: String }, AccountDeactivate, CrossSigningReset, } -impl From for AccountManagementActionFull { - fn from(value: AccountManagementAction) -> Self { +impl<'a> From<&'a AccountManagementAction> for AccountManagementActionData<'a> { + fn from(value: &'a AccountManagementAction) -> Self { match value { AccountManagementAction::Profile => Self::Profile, - AccountManagementAction::SessionsList => Self::SessionsList, - AccountManagementAction::SessionView { device_id } => { - Self::SessionView { device_id: device_id.into() } + AccountManagementAction::DevicesList => Self::DevicesList, + AccountManagementAction::DeviceView { device_id } => { + Self::DeviceView(DeviceViewData::new(device_id.as_str().into())) } - AccountManagementAction::SessionEnd { device_id } => { - Self::SessionEnd { device_id: device_id.into() } + AccountManagementAction::DeviceDelete { device_id } => { + Self::DeviceDelete(DeviceDeleteData::new(device_id.as_str().into())) } AccountManagementAction::AccountDeactivate => Self::AccountDeactivate, AccountManagementAction::CrossSigningReset => Self::CrossSigningReset, diff --git a/crates/matrix-sdk/CHANGELOG.md b/crates/matrix-sdk/CHANGELOG.md index 78f42680f..ca467ebfe 100644 --- a/crates/matrix-sdk/CHANGELOG.md +++ b/crates/matrix-sdk/CHANGELOG.md @@ -8,6 +8,9 @@ All notable changes to this project will be documented in this file. ### Features +- Add `OAuth::cached_server_metadata()` that caches the authorization server + metadata for a while. + ([#6217](https://github.com/matrix-org/matrix-rust-sdk/pull/6217)) - Add `QRCodeGrantLoginError::SecureChannel` for secure channel errors ([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141) - Add `QRCodeGrantLoginError::UnexpectedMessage` for protocol message errors @@ -79,6 +82,10 @@ All notable changes to this project will be documented in this file. ### Refactor +- [**breaking**] The functions on the `OAuth` API to access the account + management URL and its actions were removed. The methods available on the + `AuthorizationServerMetadata` should be used instead. + ([#6217](https://github.com/matrix-org/matrix-rust-sdk/pull/6217)) - [**breaking**] `QRCodeGrantLoginError::UnableToCreateDevice` has been removed ([#6141](https://github.com/matrix-org/matrix-rust-sdk/pull/6141) - The `RoomEventCache::paginate_thread_backwards` method is replaced by `RoomEventCache::thread_pagination` which returns a new `ThreadPagination` type, similar to `RoomPagination`. diff --git a/crates/matrix-sdk/src/authentication/oauth/account_management_url.rs b/crates/matrix-sdk/src/authentication/oauth/account_management_url.rs deleted file mode 100644 index 077c5aa75..000000000 --- a/crates/matrix-sdk/src/authentication/oauth/account_management_url.rs +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2025 Kévin Commaille -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! Types and functions related to the account management URL. -//! -//! This is a Matrix extension introduced in [MSC4191](https://github.com/matrix-org/matrix-spec-proposals/pull/4191). - -use ruma::{ - OwnedDeviceId, - api::client::discovery::get_authorization_server_metadata::v1::AccountManagementAction, -}; -use url::Url; - -/// An account management action that a user can take, including a device ID for -/// the actions that support it. -/// -/// The actions are defined in [MSC4191]. -/// -/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191 -#[derive(Debug, Clone, PartialEq, Eq)] -#[non_exhaustive] -pub enum AccountManagementActionFull { - /// `org.matrix.profile` - /// - /// The user wishes to view their profile (name, avatar, contact details). - Profile, - - /// `org.matrix.sessions_list` - /// - /// The user wishes to view a list of their sessions. - SessionsList, - - /// `org.matrix.session_view` - /// - /// The user wishes to view the details of a specific session. - SessionView { - /// The ID of the session to view the details of. - device_id: OwnedDeviceId, - }, - - /// `org.matrix.session_end` - /// - /// The user wishes to end/log out of a specific session. - SessionEnd { - /// The ID of the session to end. - device_id: OwnedDeviceId, - }, - - /// `org.matrix.account_deactivate` - /// - /// The user wishes to deactivate their account. - AccountDeactivate, - - /// `org.matrix.cross_signing_reset` - /// - /// The user wishes to reset their cross-signing keys. - CrossSigningReset, -} - -impl AccountManagementActionFull { - /// Get the [`AccountManagementAction`] matching this - /// [`AccountManagementActionFull`]. - pub fn action_type(&self) -> AccountManagementAction { - match self { - Self::Profile => AccountManagementAction::Profile, - Self::SessionsList => AccountManagementAction::UnstableSessionsList, - Self::SessionView { .. } => AccountManagementAction::UnstableSessionView, - Self::SessionEnd { .. } => AccountManagementAction::UnstableSessionEnd, - Self::AccountDeactivate => AccountManagementAction::AccountDeactivate, - Self::CrossSigningReset => AccountManagementAction::CrossSigningReset, - } - } - - /// Append this action to the query of the given URL. - fn append_to_url(&self, url: &mut Url) { - let mut query_pairs = url.query_pairs_mut(); - query_pairs.append_pair("action", self.action_type().as_str()); - - match self { - Self::SessionView { device_id } | Self::SessionEnd { device_id } => { - query_pairs.append_pair("device_id", device_id.as_str()); - } - _ => {} - } - } -} - -/// Builder for the URL for accessing the account management capabilities, as -/// defined in [MSC4191]. -/// -/// This type can be instantiated with [`OAuth::account_management_url()`] and -/// [`OAuth::fetch_account_management_url()`]. -/// -/// [`AccountManagementUrlBuilder::build()`] returns a URL to be opened in a web -/// browser where the end-user will be able to access the account management -/// capabilities of the issuer. -/// -/// # Example -/// -/// ```no_run -/// use matrix_sdk::authentication::oauth::AccountManagementActionFull; -/// # _ = async { -/// # let client: matrix_sdk::Client = unimplemented!(); -/// let oauth = client.oauth(); -/// -/// // Get the account management URL from the server metadata. -/// let Some(url_builder) = oauth.account_management_url().await? else { -/// println!("The server doesn't advertise an account management URL"); -/// return Ok(()); -/// }; -/// -/// // The user wants to see the list of sessions. -/// let url = -/// url_builder.action(AccountManagementActionFull::SessionsList).build(); -/// -/// println!("See your sessions at: {url}"); -/// # anyhow::Ok(()) }; -/// ``` -/// -/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191 -/// [`OAuth::account_management_url()`]: super::OAuth::account_management_url -/// [`OAuth::fetch_account_management_url()`]: super::OAuth::fetch_account_management_url -#[derive(Debug, Clone)] -pub struct AccountManagementUrlBuilder { - account_management_uri: Url, - action: Option, -} - -impl AccountManagementUrlBuilder { - /// Construct an [`AccountManagementUrlBuilder`] for the given URL. - pub(super) fn new(account_management_uri: Url) -> Self { - Self { account_management_uri, action: None } - } - - /// Set the action that the user wishes to take. - pub fn action(mut self, action: AccountManagementActionFull) -> Self { - self.action = Some(action); - self - } - - /// Build the URL to present to the end user. - pub fn build(self) -> Url { - // Add our parameters to the query, because the URL might already have one. - let mut account_management_uri = self.account_management_uri; - - if let Some(action) = &self.action { - action.append_to_url(&mut account_management_uri); - } - - account_management_uri - } -} - -#[cfg(test)] -mod tests { - use ruma::owned_device_id; - use url::Url; - - use super::{AccountManagementActionFull, AccountManagementUrlBuilder}; - - #[test] - fn test_build_account_management_url_actions() { - let base_url = Url::parse("https://example.org").unwrap(); - let device_id = owned_device_id!("ABCDEFG"); - - let url = AccountManagementUrlBuilder::new(base_url.clone()).build(); - assert_eq!(url, base_url); - - let url = AccountManagementUrlBuilder::new(base_url.clone()) - .action(AccountManagementActionFull::Profile) - .build(); - assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.profile"); - - let url = AccountManagementUrlBuilder::new(base_url.clone()) - .action(AccountManagementActionFull::SessionsList) - .build(); - assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.sessions_list"); - - let url = AccountManagementUrlBuilder::new(base_url.clone()) - .action(AccountManagementActionFull::SessionView { device_id: device_id.clone() }) - .build(); - assert_eq!( - url.as_str(), - "https://example.org/?action=org.matrix.session_view&device_id=ABCDEFG" - ); - - let url = AccountManagementUrlBuilder::new(base_url.clone()) - .action(AccountManagementActionFull::SessionEnd { device_id }) - .build(); - assert_eq!( - url.as_str(), - "https://example.org/?action=org.matrix.session_end&device_id=ABCDEFG" - ); - - let url = AccountManagementUrlBuilder::new(base_url.clone()) - .action(AccountManagementActionFull::AccountDeactivate) - .build(); - assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.account_deactivate"); - - let url = AccountManagementUrlBuilder::new(base_url) - .action(AccountManagementActionFull::CrossSigningReset) - .build(); - assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.cross_signing_reset"); - } - - #[test] - fn test_build_account_management_url_with_query() { - let base_url = Url::parse("https://example.org/?sid=123456").unwrap(); - - let url = AccountManagementUrlBuilder::new(base_url.clone()) - .action(AccountManagementActionFull::Profile) - .build(); - assert_eq!(url.as_str(), "https://example.org/?sid=123456&action=org.matrix.profile"); - - let url = AccountManagementUrlBuilder::new(base_url) - .action(AccountManagementActionFull::SessionView { - device_id: owned_device_id!("ABCDEFG"), - }) - .build(); - assert_eq!( - url.as_str(), - "https://example.org/?sid=123456&action=org.matrix.session_view&device_id=ABCDEFG" - ); - } -} diff --git a/crates/matrix-sdk/src/authentication/oauth/mod.rs b/crates/matrix-sdk/src/authentication/oauth/mod.rs index efed6e6c4..37d988d28 100644 --- a/crates/matrix-sdk/src/authentication/oauth/mod.rs +++ b/crates/matrix-sdk/src/authentication/oauth/mod.rs @@ -134,11 +134,10 @@ //! account. It can be used to replace most of the Matrix APIs requiring //! User-Interactive Authentication. //! -//! An [`AccountManagementUrlBuilder`] can be obtained with -//! [`OAuth::account_management_url()`]. Then the action that the user wants to -//! perform can be customized with [`AccountManagementUrlBuilder::action()`]. -//! Finally you can obtain the final URL to present to the user with -//! [`AccountManagementUrlBuilder::build()`]. +//! The account management URL is available as `account_management_uri` on +//! [`AuthorizationServerMetadata`]. To build a full account management URL that +//! includes the action that the user wants to perform, use +//! [`AuthorizationServerMetadata::account_management_url_with_action()`]. //! //! # Logout //! @@ -164,12 +163,7 @@ use std::sync::OnceLock; #[cfg(feature = "e2e-encryption")] use std::time::Duration; -use std::{ - borrow::Cow, - collections::{BTreeSet, HashMap}, - fmt, - sync::Arc, -}; +use std::{borrow::Cow, collections::HashMap, fmt, sync::Arc}; use as_variant::as_variant; #[cfg(feature = "e2e-encryption")] @@ -192,8 +186,7 @@ pub use oauth2::{ClientId, CsrfToken}; use ruma::{ DeviceId, OwnedDeviceId, api::client::discovery::get_authorization_server_metadata::{ - self, - v1::{AccountManagementAction, AuthorizationServerMetadata}, + self, v1::AuthorizationServerMetadata, }, serde::Raw, }; @@ -203,7 +196,6 @@ use tokio::sync::Mutex; use tracing::{debug, error, instrument, trace, warn}; use url::Url; -mod account_management_url; mod auth_code_builder; #[cfg(feature = "e2e-encryption")] mod cross_process; @@ -223,7 +215,6 @@ use self::qrcode::{ LoginWithQrCode, }; pub use self::{ - account_management_url::{AccountManagementActionFull, AccountManagementUrlBuilder}, auth_code_builder::{OAuthAuthCodeUrlBuilder, OAuthAuthorizationData}, error::OAuthError, }; @@ -431,61 +422,24 @@ impl OAuth { Ok(()) } - /// The account management actions supported by the authorization server's - /// account management URL. + /// Get the cached OAuth 2.0 authorization server metadata of the + /// homeserver. /// - /// Returns an error if the request to get the server metadata fails. - pub async fn account_management_actions_supported( - &self, - ) -> Result, OAuthError> { - let server_metadata = self.server_metadata().await?; - - Ok(server_metadata.account_management_actions_supported) - } - - /// Get the account management URL where the user can manage their - /// identity-related settings. - /// - /// This will always request the latest server metadata to get the account - /// management URL. - /// - /// To avoid making a request each time, you can use - /// [`OAuth::account_management_url()`]. - /// - /// Returns an [`AccountManagementUrlBuilder`] if the URL was found. An - /// optional action to perform can be added with `.action()`, and the final - /// URL is obtained with `.build()`. - /// - /// Returns `Ok(None)` if the URL was not found. - /// - /// Returns an error if the request to get the server metadata fails or the - /// URL could not be parsed. - pub async fn fetch_account_management_url( - &self, - ) -> Result, OAuthError> { - let server_metadata = self.server_metadata().await?; - Ok(server_metadata.account_management_uri.map(AccountManagementUrlBuilder::new)) - } - - /// Get the account management URL where the user can manage their - /// identity-related settings. - /// - /// This method will cache the URL for a while, if the cache is not + /// This method will cache the metadata for a while. If the cache is not /// populated it will request the server metadata, like a call to - /// [`OAuth::fetch_account_management_url()`], and cache the resulting URL - /// before returning it. + /// [`OAuth::server_metadata()`], and cache the response before returning + /// it. /// - /// Returns an [`AccountManagementUrlBuilder`] if the URL was found. An - /// optional action to perform can be added with `.action()`, and the final - /// URL is obtained with `.build()`. + /// In most cases during the authentication process, it is better to always + /// fetch the metadata from the server. This is provided for convenience for + /// cases where the client doesn't want to incur the extra time necessary to + /// make the request. /// - /// Returns `Ok(None)` if the URL was not found. - /// - /// Returns an error if the request to get the server metadata fails or the - /// URL could not be parsed. - pub async fn account_management_url( + /// Returns an error if a problem occurred when fetching or validating the + /// metadata. + pub async fn cached_server_metadata( &self, - ) -> Result, OAuthError> { + ) -> Result { const CACHE_KEY: &str = "SERVER_METADATA"; let mut cache = self.client.inner.caches.server_metadata.lock().await; @@ -498,11 +452,16 @@ impl OAuth { server_metadata }; - Ok(metadata.account_management_uri.map(AccountManagementUrlBuilder::new)) + Ok(metadata) } /// Fetch the OAuth 2.0 authorization server metadata of the homeserver. /// + /// This will always request the latest server metadata from the homeserver. + /// + /// To avoid making a request each time, you can use + /// [`OAuth::cached_server_metadata()`]. + /// /// Returns an error if a problem occurred when fetching or validating the /// metadata. pub async fn server_metadata( diff --git a/crates/matrix-sdk/src/authentication/oauth/tests.rs b/crates/matrix-sdk/src/authentication/oauth/tests.rs index 5d8fa132d..349be5898 100644 --- a/crates/matrix-sdk/src/authentication/oauth/tests.rs +++ b/crates/matrix-sdk/src/authentication/oauth/tests.rs @@ -673,7 +673,7 @@ async fn test_register_client() { } #[async_test] -async fn test_management_url_cache() { +async fn test_server_metadata_cache() { let server = MatrixMockServer::new().await; let oauth_server = server.oauth(); @@ -685,23 +685,13 @@ async fn test_management_url_cache() { // The cache should not contain the entry. assert!(!client.inner.caches.server_metadata.lock().await.contains("SERVER_METADATA")); - let management_url = oauth - .account_management_url() - .await - .expect("We should be able to fetch the account management url"); - - assert!(management_url.is_some()); + oauth.cached_server_metadata().await.expect("We should be able to fetch the server metadata"); // Check that the server metadata has been inserted into the cache. assert!(client.inner.caches.server_metadata.lock().await.contains("SERVER_METADATA")); // Another call doesn't make another request for the metadata. - let management_url = oauth - .account_management_url() - .await - .expect("We should be able to fetch the account management url"); - - assert!(management_url.is_some()); + oauth.cached_server_metadata().await.expect("We should be able to fetch the server_metadata"); } #[async_test] diff --git a/examples/oauth_cli/src/main.rs b/examples/oauth_cli/src/main.rs index 9548934b7..46c060719 100644 --- a/examples/oauth_cli/src/main.rs +++ b/examples/oauth_cli/src/main.rs @@ -24,8 +24,7 @@ use futures_util::StreamExt; use matrix_sdk::{ Client, ClientBuildError, Result, RoomState, authentication::oauth::{ - AccountManagementActionFull, ClientId, OAuthAuthorizationData, OAuthError, OAuthSession, - UrlOrQuery, UserSession, + ClientId, OAuthAuthorizationData, OAuthError, OAuthSession, UrlOrQuery, UserSession, error::OAuthClientRegistrationError, registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType}, }, @@ -33,6 +32,7 @@ use matrix_sdk::{ encryption::{CrossSigningResetAuthType, recovery::RecoveryState}, room::Room, ruma::{ + api::client::discovery::get_authorization_server_metadata::v1::AccountManagementActionData, events::room::message::{MessageType, OriginalSyncRoomMessageEvent}, serde::Raw, }, @@ -265,10 +265,10 @@ impl OAuthCli { self.account(None).await; } Some("profile") => { - self.account(Some(AccountManagementActionFull::Profile)).await; + self.account(Some(AccountManagementActionData::Profile)).await; } - Some("sessions") => { - self.account(Some(AccountManagementActionFull::SessionsList)).await; + Some("devices") => { + self.account(Some(AccountManagementActionData::DevicesList)).await; } Some("watch") => match args.next() { Some(sub) => { @@ -375,18 +375,23 @@ impl OAuthCli { } /// Get the account management URL. - async fn account(&self, action: Option) { - let Ok(Some(mut url_builder)) = self.client.oauth().fetch_account_management_url().await - else { + async fn account(&self, action: Option>) { + let Ok(server_metadata) = self.client.oauth().cached_server_metadata().await else { + println!("\nCould not retrieve the server metadata"); + return; + }; + + let url = if let Some(action) = action { + server_metadata.account_management_url_with_action(action) + } else { + server_metadata.account_management_uri + }; + + let Some(url) = url else { println!("\nThis homeserver does not provide the URL to manage your account"); return; }; - if let Some(action) = action { - url_builder = url_builder.action(action); - } - - let url = url_builder.build(); println!("\nTo manage your account, visit: {url}"); }