Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit 4eeedbe

Browse files
zecakehsandhose
authored andcommitted
Add account management URL for clients
Signed-off-by: Kévin Commaille <[email protected]>
1 parent d8f5fda commit 4eeedbe

File tree

5 files changed

+347
-0
lines changed

5 files changed

+347
-0
lines changed

crates/oauth2-types/src/oidc.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,78 @@ pub enum ClaimType {
164164
Distributed,
165165
}
166166

167+
/// An account management action that a user can take.
168+
///
169+
/// Source: <https://github.com/matrix-org/matrix-spec-proposals/pull/2965>
170+
#[derive(
171+
SerializeDisplay, DeserializeFromStr, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash,
172+
)]
173+
#[non_exhaustive]
174+
pub enum AccountManagementAction {
175+
/// `org.matrix.profile`
176+
///
177+
/// The user wishes to view their profile (name, avatar, contact details).
178+
Profile,
179+
180+
/// `org.matrix.sessions_list`
181+
///
182+
/// The user wishes to view a list of their sessions.
183+
SessionsList,
184+
185+
/// `org.matrix.session_view`
186+
///
187+
/// The user wishes to view the details of a specific session.
188+
SessionView,
189+
190+
/// `org.matrix.session_end`
191+
///
192+
/// The user wishes to end/log out of a specific session.
193+
SessionEnd,
194+
195+
/// `org.matrix.account_deactivate`
196+
///
197+
/// The user wishes to deactivate their account.
198+
AccountDeactivate,
199+
200+
/// `org.matrix.cross_signing_reset`
201+
///
202+
/// The user wishes to reset their cross-signing keys.
203+
CrossSigningReset,
204+
205+
/// An unknown value.
206+
Unknown(String),
207+
}
208+
209+
impl core::fmt::Display for AccountManagementAction {
210+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
211+
match self {
212+
Self::Profile => write!(f, "org.matrix.profile"),
213+
Self::SessionsList => write!(f, "org.matrix.sessions_list"),
214+
Self::SessionView => write!(f, "org.matrix.session_view"),
215+
Self::SessionEnd => write!(f, "org.matrix.session_end"),
216+
Self::AccountDeactivate => write!(f, "org.matrix.account_deactivate"),
217+
Self::CrossSigningReset => write!(f, "org.matrix.cross_signing_reset"),
218+
Self::Unknown(value) => write!(f, "{value}"),
219+
}
220+
}
221+
}
222+
223+
impl core::str::FromStr for AccountManagementAction {
224+
type Err = core::convert::Infallible;
225+
226+
fn from_str(s: &str) -> Result<Self, Self::Err> {
227+
match s {
228+
"org.matrix.profile" => Ok(Self::Profile),
229+
"org.matrix.sessions_list" => Ok(Self::SessionsList),
230+
"org.matrix.session_view" => Ok(Self::SessionView),
231+
"org.matrix.session_end" => Ok(Self::SessionEnd),
232+
"org.matrix.account_deactivate" => Ok(Self::AccountDeactivate),
233+
"org.matrix.cross_signing_reset" => Ok(Self::CrossSigningReset),
234+
value => Ok(Self::Unknown(value.to_owned())),
235+
}
236+
}
237+
}
238+
167239
/// The default value of `response_modes_supported` if it is not set.
168240
pub static DEFAULT_RESPONSE_MODES_SUPPORTED: &[ResponseMode] =
169241
&[ResponseMode::Query, ResponseMode::Fragment];
@@ -479,6 +551,17 @@ pub struct ProviderMetadata {
479551
///
480552
/// [RP-Initiated Logout endpoint]: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
481553
pub end_session_endpoint: Option<Url>,
554+
555+
/// URL where the user is able to access the account management capabilities
556+
/// of this OP.
557+
///
558+
/// This is a Matrix extension introduced in [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965).
559+
pub account_management_uri: Option<Url>,
560+
561+
/// Array of actions that the account management URL supports.
562+
///
563+
/// This is a Matrix extension introduced in [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965).
564+
pub account_management_actions_supported: Option<Vec<AccountManagementAction>>,
482565
}
483566

484567
impl ProviderMetadata {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2024 Kévin Commaille.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Methods related to the account management URL.
16+
//!
17+
//! This is a Matrix extension introduced in [MSC2965](https://github.com/matrix-org/matrix-spec-proposals/pull/2965).
18+
19+
use serde::Serialize;
20+
use serde_with::skip_serializing_none;
21+
use url::Url;
22+
23+
/// An account management action that a user can take, including a device ID for
24+
/// the actions that support it.
25+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
26+
#[serde(tag = "action")]
27+
#[non_exhaustive]
28+
pub enum AccountManagementActionFull {
29+
/// `org.matrix.profile`
30+
///
31+
/// The user wishes to view their profile (name, avatar, contact details).
32+
#[serde(rename = "org.matrix.profile")]
33+
Profile,
34+
35+
/// `org.matrix.sessions_list`
36+
///
37+
/// The user wishes to view a list of their sessions.
38+
#[serde(rename = "org.matrix.sessions_list")]
39+
SessionsList,
40+
41+
/// `org.matrix.session_view`
42+
///
43+
/// The user wishes to view the details of a specific session.
44+
#[serde(rename = "org.matrix.session_view")]
45+
SessionView {
46+
/// The ID of the session to view the details of.
47+
device_id: String,
48+
},
49+
50+
/// `org.matrix.session_end`
51+
///
52+
/// The user wishes to end/log out of a specific session.
53+
#[serde(rename = "org.matrix.session_end")]
54+
SessionEnd {
55+
/// The ID of the session to end.
56+
device_id: String,
57+
},
58+
59+
/// `org.matrix.account_deactivate`
60+
///
61+
/// The user wishes to deactivate their account.
62+
#[serde(rename = "org.matrix.account_deactivate")]
63+
AccountDeactivate,
64+
65+
/// `org.matrix.cross_signing_reset`
66+
///
67+
/// The user wishes to reset their cross-signing keys.
68+
#[serde(rename = "org.matrix.cross_signing_reset")]
69+
CrossSigningReset,
70+
}
71+
72+
#[skip_serializing_none]
73+
#[derive(Debug, Clone, Serialize)]
74+
struct AccountManagementData {
75+
#[serde(flatten)]
76+
action: Option<AccountManagementActionFull>,
77+
id_token_hint: Option<String>,
78+
}
79+
80+
/// Build the URL for accessing the account management capabilities.
81+
///
82+
/// # Arguments
83+
///
84+
/// * `account_management_uri` - The URL to access the issuer's account
85+
/// management capabilities.
86+
///
87+
/// * `action` - The action that the user wishes to take.
88+
///
89+
/// * `id_token_hint` - An ID Token that was previously issued to the client,
90+
/// used as a hint for which user is requesting to manage their account.
91+
///
92+
/// # Returns
93+
///
94+
/// A URL to be opened in a web browser where the end-user will be able to
95+
/// access the account management capabilities of the issuer.
96+
///
97+
/// # Errors
98+
///
99+
/// Returns an error if serializing the URL fails.
100+
pub fn build_account_management_url(
101+
mut account_management_uri: Url,
102+
action: Option<AccountManagementActionFull>,
103+
id_token_hint: Option<String>,
104+
) -> Result<Url, serde_urlencoded::ser::Error> {
105+
let data = AccountManagementData {
106+
action,
107+
id_token_hint,
108+
};
109+
let extra_query = serde_urlencoded::to_string(data)?;
110+
111+
if !extra_query.is_empty() {
112+
// Add our parameters to the query, because the URL might already have one.
113+
let mut full_query = account_management_uri
114+
.query()
115+
.map(ToOwned::to_owned)
116+
.unwrap_or_default();
117+
118+
if !full_query.is_empty() {
119+
full_query.push('&');
120+
}
121+
full_query.push_str(&extra_query);
122+
123+
account_management_uri.set_query(Some(&full_query));
124+
}
125+
126+
Ok(account_management_uri)
127+
}

crates/oidc-client/src/requests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
//! Methods to interact with OpenID Connect and OAuth2.0 endpoints.
1616
17+
pub mod account_management;
1718
pub mod authorization_code;
1819
pub mod client_credentials;
1920
pub mod discovery;
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
// Copyright 2024 Kévin Commaille.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::collections::HashMap;
16+
17+
use mas_oidc_client::requests::account_management::{
18+
build_account_management_url, AccountManagementActionFull,
19+
};
20+
use url::Url;
21+
22+
#[test]
23+
fn build_url() {
24+
let account_management_uri = Url::parse("http://localhost/account_management/").unwrap();
25+
26+
// No params
27+
let url = build_account_management_url(account_management_uri.clone(), None, None).unwrap();
28+
29+
assert_eq!(url.query(), None);
30+
31+
// Action without device ID.
32+
let url = build_account_management_url(
33+
account_management_uri.clone(),
34+
Some(AccountManagementActionFull::Profile),
35+
None,
36+
)
37+
.unwrap();
38+
39+
let query_pairs = url.query_pairs().collect::<HashMap<_, _>>();
40+
assert_eq!(query_pairs.len(), 1);
41+
assert_eq!(query_pairs.get("action").unwrap(), "org.matrix.profile");
42+
43+
// Action with device ID.
44+
let url = build_account_management_url(
45+
account_management_uri.clone(),
46+
Some(AccountManagementActionFull::SessionEnd {
47+
device_id: "mydevice".to_owned(),
48+
}),
49+
None,
50+
)
51+
.unwrap();
52+
53+
let query_pairs = url.query_pairs().collect::<HashMap<_, _>>();
54+
assert_eq!(query_pairs.len(), 2);
55+
assert_eq!(query_pairs.get("action").unwrap(), "org.matrix.session_end");
56+
assert_eq!(query_pairs.get("device_id").unwrap(), "mydevice");
57+
58+
// ID Token hint.
59+
let url = build_account_management_url(
60+
account_management_uri.clone(),
61+
None,
62+
Some("anidtokenthat.might.looksomethinglikethis".to_owned()),
63+
)
64+
.unwrap();
65+
66+
let query_pairs = url.query_pairs().collect::<HashMap<_, _>>();
67+
assert_eq!(query_pairs.len(), 1);
68+
assert_eq!(
69+
query_pairs.get("id_token_hint").unwrap(),
70+
"anidtokenthat.might.looksomethinglikethis"
71+
);
72+
73+
// Action without device ID and ID Token hint.
74+
let url = build_account_management_url(
75+
account_management_uri.clone(),
76+
Some(AccountManagementActionFull::AccountDeactivate),
77+
Some("anotheridtokenthat.might.looksomethinglikethis".to_owned()),
78+
)
79+
.unwrap();
80+
81+
let query_pairs = url.query_pairs().collect::<HashMap<_, _>>();
82+
assert_eq!(query_pairs.len(), 2);
83+
assert_eq!(
84+
query_pairs.get("action").unwrap(),
85+
"org.matrix.account_deactivate"
86+
);
87+
assert_eq!(
88+
query_pairs.get("id_token_hint").unwrap(),
89+
"anotheridtokenthat.might.looksomethinglikethis"
90+
);
91+
92+
// Action with device ID and ID Token hint.
93+
let url = build_account_management_url(
94+
account_management_uri,
95+
Some(AccountManagementActionFull::SessionView {
96+
device_id: "myseconddevice".to_owned(),
97+
}),
98+
Some("athirdidtokenthat.might.looksomethinglikethis".to_owned()),
99+
)
100+
.unwrap();
101+
102+
let query_pairs = url.query_pairs().collect::<HashMap<_, _>>();
103+
assert_eq!(query_pairs.len(), 3);
104+
assert_eq!(
105+
query_pairs.get("action").unwrap(),
106+
"org.matrix.session_view"
107+
);
108+
assert_eq!(query_pairs.get("device_id").unwrap(), "myseconddevice");
109+
assert_eq!(
110+
query_pairs.get("id_token_hint").unwrap(),
111+
"athirdidtokenthat.might.looksomethinglikethis"
112+
);
113+
114+
// Account management URI with a query already.
115+
let account_management_uri_with_query =
116+
Url::parse("http://localhost/account_management?param=value").unwrap();
117+
118+
let url = build_account_management_url(
119+
account_management_uri_with_query,
120+
Some(AccountManagementActionFull::SessionsList),
121+
Some("afinalidtokenthat.might.looksomethinglikethis".to_owned()),
122+
)
123+
.unwrap();
124+
125+
let query_pairs = url.query_pairs().collect::<HashMap<_, _>>();
126+
assert_eq!(query_pairs.len(), 3);
127+
assert_eq!(
128+
query_pairs.get("action").unwrap(),
129+
"org.matrix.sessions_list"
130+
);
131+
assert_eq!(
132+
query_pairs.get("id_token_hint").unwrap(),
133+
"afinalidtokenthat.might.looksomethinglikethis"
134+
);
135+
}

crates/oidc-client/tests/it/requests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
mod account_management;
1516
mod authorization_code;
1617
mod client_credentials;
1718
mod discovery;

0 commit comments

Comments
 (0)