refactor(sdk): Remove methods on OAuth API for account management URL

Instead encourage users to use the ones available on
`AuthorizationServerMetadata` because they support both the stable and
unstable actions.

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
This commit is contained in:
Kévin Commaille
2026-02-26 14:52:05 +01:00
committed by Damir Jelić
parent c50bab4847
commit 2d13a682a2
7 changed files with 82 additions and 350 deletions
+6
View File
@@ -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`
+23 -22
View File
@@ -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<String, ClientError> {
@@ -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<AccountManagementAction> 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,
+7
View File
@@ -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`.
@@ -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<AccountManagementActionFull>,
}
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"
);
}
}
@@ -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<BTreeSet<AccountManagementAction>, 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<Option<AccountManagementUrlBuilder>, 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<Option<AccountManagementUrlBuilder>, OAuthError> {
) -> Result<AuthorizationServerMetadata, OAuthDiscoveryError> {
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(
@@ -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]
+18 -13
View File
@@ -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<AccountManagementActionFull>) {
let Ok(Some(mut url_builder)) = self.client.oauth().fetch_account_management_url().await
else {
async fn account(&self, action: Option<AccountManagementActionData<'_>>) {
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}");
}