From 9adff21f78c997f3fa7430bb805e4d74535674da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Thu, 6 Mar 2025 14:23:46 +0100 Subject: [PATCH] refactor(oidc): Import code for building the account management URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- bindings/matrix-sdk-ffi/src/client.rs | 12 +- .../oidc/account_management_url.rs | 191 ++++++++++++++++++ .../src/authentication/oidc/error.rs | 4 + .../matrix-sdk/src/authentication/oidc/mod.rs | 19 +- .../src/authentication/oidc/tests.rs | 5 +- examples/oidc_cli/src/main.rs | 5 +- 6 files changed, 220 insertions(+), 16 deletions(-) create mode 100644 crates/matrix-sdk/src/authentication/oidc/account_management_url.rs diff --git a/bindings/matrix-sdk-ffi/src/client.rs b/bindings/matrix-sdk-ffi/src/client.rs index 05c4a614f..dc5adbc23 100644 --- a/bindings/matrix-sdk-ffi/src/client.rs +++ b/bindings/matrix-sdk-ffi/src/client.rs @@ -8,8 +8,8 @@ use anyhow::{anyhow, Context as _}; use async_compat::get_runtime_handle; use matrix_sdk::{ authentication::oidc::{ - registrations::ClientId, requests::account_management::AccountManagementActionFull, - types::requests::Prompt as SdkOidcPrompt, OidcAuthorizationData, OidcSession, + registrations::ClientId, types::requests::Prompt as SdkOidcPrompt, + AccountManagementActionFull, OidcAuthorizationData, OidcSession, }, event_cache::EventCacheError, media::{ @@ -1723,8 +1723,12 @@ impl From for AccountManagementActionFull { match value { AccountManagementAction::Profile => Self::Profile, AccountManagementAction::SessionsList => Self::SessionsList, - AccountManagementAction::SessionView { device_id } => Self::SessionView { device_id }, - AccountManagementAction::SessionEnd { device_id } => Self::SessionEnd { device_id }, + AccountManagementAction::SessionView { device_id } => { + Self::SessionView { device_id: device_id.into() } + } + AccountManagementAction::SessionEnd { device_id } => { + Self::SessionEnd { device_id: device_id.into() } + } AccountManagementAction::AccountDeactivate => Self::AccountDeactivate, AccountManagementAction::CrossSigningReset => Self::CrossSigningReset, } diff --git a/crates/matrix-sdk/src/authentication/oidc/account_management_url.rs b/crates/matrix-sdk/src/authentication/oidc/account_management_url.rs new file mode 100644 index 000000000..1039eb542 --- /dev/null +++ b/crates/matrix-sdk/src/authentication/oidc/account_management_url.rs @@ -0,0 +1,191 @@ +// 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; +use serde::Serialize; +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, Serialize)] +#[serde(tag = "action")] +#[non_exhaustive] +pub enum AccountManagementActionFull { + /// `org.matrix.profile` + /// + /// The user wishes to view their profile (name, avatar, contact details). + #[serde(rename = "org.matrix.profile")] + Profile, + + /// `org.matrix.sessions_list` + /// + /// The user wishes to view a list of their sessions. + #[serde(rename = "org.matrix.sessions_list")] + SessionsList, + + /// `org.matrix.session_view` + /// + /// The user wishes to view the details of a specific session. + #[serde(rename = "org.matrix.session_view")] + 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. + #[serde(rename = "org.matrix.session_end")] + SessionEnd { + /// The ID of the session to end. + device_id: OwnedDeviceId, + }, + + /// `org.matrix.account_deactivate` + /// + /// The user wishes to deactivate their account. + #[serde(rename = "org.matrix.account_deactivate")] + AccountDeactivate, + + /// `org.matrix.cross_signing_reset` + /// + /// The user wishes to reset their cross-signing keys. + #[serde(rename = "org.matrix.cross_signing_reset")] + CrossSigningReset, +} + +/// Build the URL for accessing the account management capabilities, as defined +/// in [MSC]. +/// +/// # Arguments +/// +/// * `account_management_uri` - The URL to access the issuer's account +/// management capabilities. +/// +/// * `action` - The action that the user wishes to take. +/// +/// # 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. +/// +/// # Errors +/// +/// Returns an error if serializing the action fails. +/// +/// [MSC4191]: https://github.com/matrix-org/matrix-spec-proposals/pull/4191 +pub(crate) fn build_account_management_url( + mut account_management_uri: Url, + action: AccountManagementActionFull, +) -> Result { + let extra_query = serde_html_form::to_string(action)?; + + // Add our parameters to the query, because the URL might already have one. + let mut full_query = account_management_uri.query().map(ToOwned::to_owned).unwrap_or_default(); + + if !full_query.is_empty() { + full_query.push('&'); + } + full_query.push_str(&extra_query); + + account_management_uri.set_query(Some(&full_query)); + + Ok(account_management_uri) +} + +#[cfg(test)] +mod tests { + use ruma::owned_device_id; + use url::Url; + + use super::{build_account_management_url, AccountManagementActionFull}; + + #[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 = + build_account_management_url(base_url.clone(), AccountManagementActionFull::Profile) + .unwrap(); + assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.profile"); + + let url = build_account_management_url( + base_url.clone(), + AccountManagementActionFull::SessionsList, + ) + .unwrap(); + assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.sessions_list"); + + let url = build_account_management_url( + base_url.clone(), + AccountManagementActionFull::SessionView { device_id: device_id.clone() }, + ) + .unwrap(); + assert_eq!( + url.as_str(), + "https://example.org/?action=org.matrix.session_view&device_id=ABCDEFG" + ); + + let url = build_account_management_url( + base_url.clone(), + AccountManagementActionFull::SessionEnd { device_id }, + ) + .unwrap(); + assert_eq!( + url.as_str(), + "https://example.org/?action=org.matrix.session_end&device_id=ABCDEFG" + ); + + let url = build_account_management_url( + base_url.clone(), + AccountManagementActionFull::AccountDeactivate, + ) + .unwrap(); + assert_eq!(url.as_str(), "https://example.org/?action=org.matrix.account_deactivate"); + + let url = + build_account_management_url(base_url, AccountManagementActionFull::CrossSigningReset) + .unwrap(); + 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 = + build_account_management_url(base_url.clone(), AccountManagementActionFull::Profile) + .unwrap(); + assert_eq!(url.as_str(), "https://example.org/?sid=123456&action=org.matrix.profile"); + + let url = build_account_management_url( + base_url, + AccountManagementActionFull::SessionView { device_id: owned_device_id!("ABCDEFG") }, + ) + .unwrap(); + 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/oidc/error.rs b/crates/matrix-sdk/src/authentication/oidc/error.rs index 4e9954966..0ed8c892e 100644 --- a/crates/matrix-sdk/src/authentication/oidc/error.rs +++ b/crates/matrix-sdk/src/authentication/oidc/error.rs @@ -89,6 +89,10 @@ pub enum OidcError { #[error("failed to log out: {0}")] Logout(#[from] OauthTokenRevocationError), + /// An error occurred building the account management URL. + #[error("failed to build account management URL: {0}")] + AccountManagementUrl(serde_html_form::ser::Error), + /// An error occurred parsing a URL. #[error(transparent)] Url(url::ParseError), diff --git a/crates/matrix-sdk/src/authentication/oidc/mod.rs b/crates/matrix-sdk/src/authentication/oidc/mod.rs index c76fd34ad..07b3aa01e 100644 --- a/crates/matrix-sdk/src/authentication/oidc/mod.rs +++ b/crates/matrix-sdk/src/authentication/oidc/mod.rs @@ -156,7 +156,6 @@ use error::{ use mas_oidc_client::{ http_service::HttpService, requests::{ - account_management::{build_account_management_url, AccountManagementActionFull}, discovery::{discover, insecure_discover}, registration::register_client, }, @@ -191,6 +190,7 @@ use tokio::{spawn, sync::Mutex}; use tracing::{debug, error, info, instrument, trace, warn}; use url::Url; +mod account_management_url; mod auth_code_builder; mod cross_process; pub mod error; @@ -200,15 +200,17 @@ pub mod registrations; #[cfg(test)] mod tests; -pub use self::{ - auth_code_builder::{OidcAuthCodeUrlBuilder, OidcAuthorizationData}, - error::OidcError, -}; use self::{ + account_management_url::build_account_management_url, cross_process::{CrossProcessRefreshLockGuard, CrossProcessRefreshManager}, qrcode::LoginWithQrCode, registrations::{ClientId, OidcRegistrations}, }; +pub use self::{ + account_management_url::AccountManagementActionFull, + auth_code_builder::{OidcAuthCodeUrlBuilder, OidcAuthorizationData}, + error::OidcError, +}; use super::{AuthData, SessionTokens}; use crate::{client::SessionChange, Client, HttpError, RefreshTokenError, Result}; @@ -619,7 +621,12 @@ impl Oidc { return Ok(None); }; - let url = build_account_management_url(base_url, action, None)?; + let url = if let Some(action) = action { + build_account_management_url(base_url, action) + .map_err(OidcError::AccountManagementUrl)? + } else { + base_url + }; Ok(Some(url)) } diff --git a/crates/matrix-sdk/src/authentication/oidc/tests.rs b/crates/matrix-sdk/src/authentication/oidc/tests.rs index 3337fc218..1d055d689 100644 --- a/crates/matrix-sdk/src/authentication/oidc/tests.rs +++ b/crates/matrix-sdk/src/authentication/oidc/tests.rs @@ -3,7 +3,6 @@ use std::collections::HashMap; use anyhow::Context as _; use assert_matches::assert_matches; use assert_matches2::assert_let; -use mas_oidc_client::requests::account_management::AccountManagementActionFull; use matrix_sdk_test::async_test; use oauth2::{CsrfToken, PkceCodeChallenge, RedirectUrl}; use ruma::{ @@ -25,8 +24,8 @@ use super::{ }; use crate::{ authentication::oidc::{ - error::AuthorizationCodeErrorResponseType, AuthorizationValidationData, - OauthAuthorizationCodeError, + error::AuthorizationCodeErrorResponseType, AccountManagementActionFull, + AuthorizationValidationData, OauthAuthorizationCodeError, }, test_utils::{ client::{ diff --git a/examples/oidc_cli/src/main.rs b/examples/oidc_cli/src/main.rs index 52ccd9162..c2a39d1a1 100644 --- a/examples/oidc_cli/src/main.rs +++ b/examples/oidc_cli/src/main.rs @@ -31,15 +31,14 @@ use futures_util::StreamExt; use matrix_sdk::{ authentication::oidc::{ registrations::ClientId, - requests::account_management::AccountManagementActionFull, types::{ iana::oauth::OAuthClientAuthenticationMethod, oidc::ApplicationType, registration::{ClientMetadata, Localized, VerifiedClientMetadata}, requests::GrantType, }, - AuthorizationCode, AuthorizationResponse, CsrfToken, OidcAuthorizationData, OidcSession, - UserSession, + AccountManagementActionFull, AuthorizationCode, AuthorizationResponse, CsrfToken, + OidcAuthorizationData, OidcSession, UserSession, }, config::SyncSettings, encryption::{recovery::RecoveryState, CrossSigningResetAuthType},