From 3303e939caec6371c28014b9a854bbae29e367bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Tue, 12 Mar 2024 13:48:48 +0100 Subject: [PATCH] Add account management URL for clients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Kévin Commaille --- crates/oauth2-types/src/oidc.rs | 83 +++++++++++ .../src/requests/account_management.rs | 127 ++++++++++++++++ crates/oidc-client/src/requests/mod.rs | 1 + .../tests/it/requests/account_management.rs | 135 ++++++++++++++++++ crates/oidc-client/tests/it/requests/mod.rs | 1 + 5 files changed, 347 insertions(+) create mode 100644 crates/oidc-client/src/requests/account_management.rs create mode 100644 crates/oidc-client/tests/it/requests/account_management.rs diff --git a/crates/oauth2-types/src/oidc.rs b/crates/oauth2-types/src/oidc.rs index 36b905405..01bd24f3e 100644 --- a/crates/oauth2-types/src/oidc.rs +++ b/crates/oauth2-types/src/oidc.rs @@ -164,6 +164,78 @@ pub enum ClaimType { Distributed, } +/// An account management action that a user can take. +/// +/// Source: +#[derive( + SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, +)] +#[non_exhaustive] +pub enum AccountManagementAction { + /// `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, + + /// `org.matrix.session_end` + /// + /// The user wishes to end/log out of a specific session. + SessionEnd, + + /// `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, + + /// An unknown value. + Unknown(String), +} + +impl core::fmt::Display for AccountManagementAction { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Profile => write!(f, "org.matrix.profile"), + Self::SessionsList => write!(f, "org.matrix.sessions_list"), + Self::SessionView => write!(f, "org.matrix.session_view"), + Self::SessionEnd => write!(f, "org.matrix.session_end"), + Self::AccountDeactivate => write!(f, "org.matrix.account_deactivate"), + Self::CrossSigningReset => write!(f, "org.matrix.cross_signing_reset"), + Self::Unknown(value) => write!(f, "{value}"), + } + } +} + +impl core::str::FromStr for AccountManagementAction { + type Err = core::convert::Infallible; + + fn from_str(s: &str) -> Result { + match s { + "org.matrix.profile" => Ok(Self::Profile), + "org.matrix.sessions_list" => Ok(Self::SessionsList), + "org.matrix.session_view" => Ok(Self::SessionView), + "org.matrix.session_end" => Ok(Self::SessionEnd), + "org.matrix.account_deactivate" => Ok(Self::AccountDeactivate), + "org.matrix.cross_signing_reset" => Ok(Self::CrossSigningReset), + value => Ok(Self::Unknown(value.to_owned())), + } + } +} + /// The default value of `response_modes_supported` if it is not set. pub static DEFAULT_RESPONSE_MODES_SUPPORTED: &[ResponseMode] = &[ResponseMode::Query, ResponseMode::Fragment]; @@ -479,6 +551,17 @@ pub struct ProviderMetadata { /// /// [RP-Initiated Logout endpoint]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html pub end_session_endpoint: Option, + + /// URL where the user is able to access the account management capabilities + /// of this OP. + /// + /// This is a Matrix extension introduced in [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965). + pub account_management_uri: Option, + + /// Array of actions that the account management URL supports. + /// + /// This is a Matrix extension introduced in [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965). + pub account_management_actions_supported: Option>, } impl ProviderMetadata { diff --git a/crates/oidc-client/src/requests/account_management.rs b/crates/oidc-client/src/requests/account_management.rs new file mode 100644 index 000000000..b76314c71 --- /dev/null +++ b/crates/oidc-client/src/requests/account_management.rs @@ -0,0 +1,127 @@ +// Copyright 2024 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. + +//! Methods related to the account management URL. +//! +//! This is a Matrix extension introduced in [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965). + +use serde::Serialize; +use serde_with::skip_serializing_none; +use url::Url; + +/// An account management action that a user can take, including a device ID for +/// the actions that support it. +#[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: String, + }, + + /// `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: String, + }, + + /// `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, +} + +#[skip_serializing_none] +#[derive(Debug, Clone, Serialize)] +struct AccountManagementData { + #[serde(flatten)] + action: Option, + id_token_hint: Option, +} + +/// Build the URL for accessing the account management capabilities. +/// +/// # Arguments +/// +/// * `account_management_uri` - The URL to access the issuer's account +/// management capabilities. +/// +/// * `action` - The action that the user wishes to take. +/// +/// * `id_token_hint` - An ID Token that was previously issued to the client, +/// used as a hint for which user is requesting to manage their account. +/// +/// # 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 URL fails. +pub fn build_account_management_url( + mut account_management_uri: Url, + action: Option, + id_token_hint: Option, +) -> Result { + let data = AccountManagementData { + action, + id_token_hint, + }; + let extra_query = serde_urlencoded::to_string(data)?; + + if !extra_query.is_empty() { + // 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) +} diff --git a/crates/oidc-client/src/requests/mod.rs b/crates/oidc-client/src/requests/mod.rs index e25793cce..4b08784b4 100644 --- a/crates/oidc-client/src/requests/mod.rs +++ b/crates/oidc-client/src/requests/mod.rs @@ -14,6 +14,7 @@ //! Methods to interact with OpenID Connect and OAuth2.0 endpoints. +pub mod account_management; pub mod authorization_code; pub mod client_credentials; pub mod discovery; diff --git a/crates/oidc-client/tests/it/requests/account_management.rs b/crates/oidc-client/tests/it/requests/account_management.rs new file mode 100644 index 000000000..a853846ae --- /dev/null +++ b/crates/oidc-client/tests/it/requests/account_management.rs @@ -0,0 +1,135 @@ +// Copyright 2024 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. + +use std::collections::HashMap; + +use mas_oidc_client::requests::account_management::{ + build_account_management_url, AccountManagementActionFull, +}; +use url::Url; + +#[test] +fn build_url() { + let account_management_uri = Url::parse("http://localhost/account_management/").unwrap(); + + // No params + let url = build_account_management_url(account_management_uri.clone(), None, None).unwrap(); + + assert_eq!(url.query(), None); + + // Action without device ID. + let url = build_account_management_url( + account_management_uri.clone(), + Some(AccountManagementActionFull::Profile), + None, + ) + .unwrap(); + + let query_pairs = url.query_pairs().collect::>(); + assert_eq!(query_pairs.len(), 1); + assert_eq!(query_pairs.get("action").unwrap(), "org.matrix.profile"); + + // Action with device ID. + let url = build_account_management_url( + account_management_uri.clone(), + Some(AccountManagementActionFull::SessionEnd { + device_id: "mydevice".to_owned(), + }), + None, + ) + .unwrap(); + + let query_pairs = url.query_pairs().collect::>(); + assert_eq!(query_pairs.len(), 2); + assert_eq!(query_pairs.get("action").unwrap(), "org.matrix.session_end"); + assert_eq!(query_pairs.get("device_id").unwrap(), "mydevice"); + + // ID Token hint. + let url = build_account_management_url( + account_management_uri.clone(), + None, + Some("anidtokenthat.might.looksomethinglikethis".to_owned()), + ) + .unwrap(); + + let query_pairs = url.query_pairs().collect::>(); + assert_eq!(query_pairs.len(), 1); + assert_eq!( + query_pairs.get("id_token_hint").unwrap(), + "anidtokenthat.might.looksomethinglikethis" + ); + + // Action without device ID and ID Token hint. + let url = build_account_management_url( + account_management_uri.clone(), + Some(AccountManagementActionFull::AccountDeactivate), + Some("anotheridtokenthat.might.looksomethinglikethis".to_owned()), + ) + .unwrap(); + + let query_pairs = url.query_pairs().collect::>(); + assert_eq!(query_pairs.len(), 2); + assert_eq!( + query_pairs.get("action").unwrap(), + "org.matrix.account_deactivate" + ); + assert_eq!( + query_pairs.get("id_token_hint").unwrap(), + "anotheridtokenthat.might.looksomethinglikethis" + ); + + // Action with device ID and ID Token hint. + let url = build_account_management_url( + account_management_uri, + Some(AccountManagementActionFull::SessionView { + device_id: "myseconddevice".to_owned(), + }), + Some("athirdidtokenthat.might.looksomethinglikethis".to_owned()), + ) + .unwrap(); + + let query_pairs = url.query_pairs().collect::>(); + assert_eq!(query_pairs.len(), 3); + assert_eq!( + query_pairs.get("action").unwrap(), + "org.matrix.session_view" + ); + assert_eq!(query_pairs.get("device_id").unwrap(), "myseconddevice"); + assert_eq!( + query_pairs.get("id_token_hint").unwrap(), + "athirdidtokenthat.might.looksomethinglikethis" + ); + + // Account management URI with a query already. + let account_management_uri_with_query = + Url::parse("http://localhost/account_management?param=value").unwrap(); + + let url = build_account_management_url( + account_management_uri_with_query, + Some(AccountManagementActionFull::SessionsList), + Some("afinalidtokenthat.might.looksomethinglikethis".to_owned()), + ) + .unwrap(); + + let query_pairs = url.query_pairs().collect::>(); + assert_eq!(query_pairs.len(), 3); + assert_eq!( + query_pairs.get("action").unwrap(), + "org.matrix.sessions_list" + ); + assert_eq!( + query_pairs.get("id_token_hint").unwrap(), + "afinalidtokenthat.might.looksomethinglikethis" + ); +} diff --git a/crates/oidc-client/tests/it/requests/mod.rs b/crates/oidc-client/tests/it/requests/mod.rs index 1a2194ef4..75f124f81 100644 --- a/crates/oidc-client/tests/it/requests/mod.rs +++ b/crates/oidc-client/tests/it/requests/mod.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +mod account_management; mod authorization_code; mod client_credentials; mod discovery;