Add account management URL for clients

Signed-off-by: Kévin Commaille <zecakeh@tedomum.fr>
This commit is contained in:
Kévin Commaille
2024-03-12 13:48:48 +01:00
committed by Quentin Gliech
parent 7dd59c962c
commit 3303e939ca
5 changed files with 347 additions and 0 deletions

View File

@@ -164,6 +164,78 @@ pub enum ClaimType {
Distributed,
}
/// An account management action that a user can take.
///
/// Source: <https://github.com/matrix-org/matrix-spec-proposals/pull/2965>
#[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<Self, Self::Err> {
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>,
/// 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<Url>,
/// 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<Vec<AccountManagementAction>>,
}
impl ProviderMetadata {

View File

@@ -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<AccountManagementActionFull>,
id_token_hint: Option<String>,
}
/// 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<AccountManagementActionFull>,
id_token_hint: Option<String>,
) -> Result<Url, serde_urlencoded::ser::Error> {
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)
}

View File

@@ -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;

View File

@@ -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::<HashMap<_, _>>();
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::<HashMap<_, _>>();
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::<HashMap<_, _>>();
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::<HashMap<_, _>>();
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::<HashMap<_, _>>();
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::<HashMap<_, _>>();
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"
);
}

View File

@@ -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;