feat: Add support for MSC4363

This commit is contained in:
Ginger
2026-10-01 14:50:16 -04:00
parent c18b01dc2d
commit 4e01ccfc36
12 changed files with 252 additions and 160 deletions
Generated
+23 -24
View File
@@ -4962,8 +4962,8 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.17.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"assign",
"js_int",
@@ -4981,8 +4981,8 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.17.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"http",
"js_int",
@@ -4994,8 +4994,8 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.24.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.25.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"as_variant",
"assign",
@@ -5016,11 +5016,11 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.19.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.20.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"as_variant",
"base64 0.22.1",
"base64 0.23.1",
"bytes",
"date_header",
"form_urlencoded",
@@ -5049,8 +5049,8 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.34.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.35.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"as_variant",
"indexmap 2.14.2",
@@ -5070,11 +5070,10 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.16.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"bytes",
"headers",
"http",
"http-auth",
"httparse",
@@ -5094,7 +5093,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.12.1"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"js_int",
"thiserror 2.0.21",
@@ -5102,8 +5101,8 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.19.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.20.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"as_variant",
"cfg-if",
@@ -5118,8 +5117,8 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.16.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"js_int",
"ruma-common",
@@ -5130,10 +5129,10 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.21.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.22.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"base64 0.22.1",
"base64 0.23.1",
"ed25519-dalek 3.0.0",
"memchr",
"pkcs8 0.11.0",
@@ -5147,8 +5146,8 @@ dependencies = [
[[package]]
name = "ruma-state-res"
version = "0.17.0"
source = "git+https://github.com/ruma/ruma.git?rev=fe8570dbca23f4263165748ed898aad126b23d85#fe8570dbca23f4263165748ed898aad126b23d85"
version = "0.18.0"
source = "git+https://github.com/gingershaped/ruwuma.git?rev=ea474080ab00d68edd484871d57488cd76a36259#ea474080ab00d68edd484871d57488cd76a36259"
dependencies = [
"js_int",
"ruma-common",
+3 -2
View File
@@ -346,8 +346,8 @@ version = "1.1.1"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
git = "https://github.com/ruma/ruma.git"
rev = "fe8570dbca23f4263165748ed898aad126b23d85"
git = "https://github.com/gingershaped/ruwuma.git"
rev = "ea474080ab00d68edd484871d57488cd76a36259"
features = [
"appservice-api-c",
"client-api",
@@ -378,6 +378,7 @@ features = [
"unstable-msc4380",
"unstable-msc4143",
"unstable-msc4293",
"unstable-msc4363",
"unstable-msc4406",
"unstable-msc4439",
"unstable-msc4354",
+11 -5
View File
@@ -18,7 +18,7 @@
AuthScheme, NoAccessToken, NoAuthentication,
},
client,
error::{ErrorKind, UnknownTokenErrorData},
error::{ErrorKind, InsufficientUserAuthenticationErrorData, UnknownTokenErrorData},
federation::authentication::ServerSignatures,
},
assign,
@@ -315,7 +315,7 @@ async fn check_access_token(
.get_session_info_for_device(&sender_user, &sender_device)
.await
{
session_info.scopes
session_info.scopes()
} else {
let mut scopes = BTreeSet::from_iter([OAuthClientScope::ApiFullAccess]);
@@ -330,9 +330,15 @@ async fn check_access_token(
.iter()
.any(|scope| user_scopes.contains(scope))
{
return Err!(Request(Forbidden(
"You do not have permission to access this endpoint."
)));
return Err(Error::Request(
ErrorKind::InsufficientUserAuthentication(Box::new(
assign!(InsufficientUserAuthenticationErrorData::new(), {
scope: required_scopes.iter().cloned().collect()
}),
)),
"You do not have permission to access this endpoint.".into(),
StatusCode::UNAUTHORIZED,
));
}
Ok(ClientIdentity::User { sender_user, sender_device })
+35 -2
View File
@@ -22,7 +22,8 @@
use futures::{FutureExt, StreamExt, TryStreamExt};
use itertools::Itertools;
use ruma::{
OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, UserId,
api::OAuthClientScope,
events::{
AnyStrippedStateEvent, GlobalAccountDataEventType, StateEventType,
push_rules::PushRulesEvent,
@@ -31,7 +32,7 @@
push::Ruleset,
serde::{Raw, from_raw_json_value},
};
use serde_json::value::to_raw_value;
use serde_json::{Value, value::to_raw_value};
use crate::{Services, media, rooms, rooms::short::ShortStateHash};
@@ -160,6 +161,10 @@ macro_rules! migration {
// This is backwards compatible because old versions will recreate the keyspace if it's
// missing.
migration!(drop_server_signingkeys),
// v22: Old versions will fail to deserialize the fixed OAuth scope names,
// preventing users from signing in.
VersionBump(22),
migration!(fix_oauth_scope_names),
]
};
@@ -936,3 +941,31 @@ async fn drop_server_signingkeys(services: &Services) -> Result<()> {
Ok(())
}
async fn fix_oauth_scope_names(services: &Services) -> Result {
let db = &services.db;
let userdeviceid_oauthsessioninfo = db["userdeviceid_oauthsessioninfo"].clone();
userdeviceid_oauthsessioninfo
.stream::<(OwnedUserId, OwnedDeviceId), Value>()
.ignore_err()
.for_each(async |((user_id, device_id), mut session_info)| {
let map = session_info.as_object_mut().unwrap();
let scopes = map.get_mut("scopes").unwrap().as_array_mut().unwrap();
if let Some(old_scope_index) = scopes
.iter()
.position(|value| *value == Value::String("ClientApi".to_owned()))
{
scopes[old_scope_index] =
Value::String(OAuthClientScope::ApiFullAccess.to_string());
}
userdeviceid_oauthsessioninfo.put((user_id, device_id), Json(session_info));
})
.await;
info!("Fixed OAuth session scope names");
Ok(())
}
+12 -6
View File
@@ -1,7 +1,7 @@
use std::{collections::BTreeSet, hash::Hash};
use std::{collections::BTreeSet, fmt::Debug, hash::Hash};
use itertools::Itertools;
use serde::{Deserialize, Deserializer, Serialize};
use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
@@ -150,7 +150,7 @@ pub enum ResponseType {
pub(super) fn btreeset_skip_err<'de, D, V>(de: D) -> Result<BTreeSet<V>, D::Error>
where
D: Deserializer<'de>,
V: Deserialize<'de> + Hash + Eq + Ord,
V: DeserializeOwned + Hash + Eq + Ord + Debug,
{
use std::marker::PhantomData;
@@ -162,7 +162,7 @@ struct BTreeSetVisitor<V> {
impl<'de, V> Visitor<'de> for BTreeSetVisitor<V>
where
V: Deserialize<'de> + Hash + Eq + Ord,
V: DeserializeOwned + Hash + Eq + Ord + Debug,
{
type Value = BTreeSet<V>;
@@ -176,8 +176,14 @@ fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
{
let mut set = BTreeSet::new();
while let Some(element) = seq.next_element().transpose() {
if let Ok(element) = element {
while let Some(element) = seq.next_element::<serde_json::Value>().transpose() {
// This trip through serde_json::Value is necessary because the
// serde_json serializer will not advance to the next
// element if an element fails to deserialize. Without it this
// would loop forever.
if let Ok(element) = element
&& let Ok(element) = serde_json::from_value(element)
{
set.insert(element);
}
}
+23 -40
View File
@@ -1,10 +1,8 @@
use std::{
borrow::Cow,
collections::HashSet,
collections::BTreeSet,
error::Error,
fmt::{Debug, Display},
hash::Hash,
mem::discriminant,
};
use regex::regex;
@@ -82,42 +80,25 @@ pub enum Prompt {
Unknown,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub enum RequestedScope {
Device(OwnedDeviceId),
ClientApi,
ServerAdministration,
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct RequestedScopes {
pub device_id: Option<OwnedDeviceId>,
pub scopes: BTreeSet<OAuthClientScope>,
}
impl RequestedScope {
#[must_use]
pub fn as_client_scope(&self) -> Option<OAuthClientScope> {
match self {
| Self::ClientApi => Some(OAuthClientScope::ApiFullAccess),
| Self::Device(_) => None,
| Self::ServerAdministration => Some(OAuthClientScope::ServerAdministration),
}
}
}
impl PartialEq for RequestedScope {
fn eq(&self, other: &Self) -> bool { discriminant(self) == discriminant(other) }
}
impl Eq for RequestedScope {}
impl Hash for RequestedScope {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { discriminant(self).hash(state); }
}
impl Display for RequestedScope {
impl Display for RequestedScopes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
| Self::ClientApi => write!(f, "urn:matrix:client:api:*"),
| Self::Device(device_id) => write!(f, "urn:matrix:client:device:{device_id}"),
| Self::ServerAdministration =>
write!(f, "urn:matrix:client:cc.c10y.msc4484.server_administration"),
let mut scopes: Vec<_> = self
.scopes
.iter()
.map(OAuthClientScope::to_string)
.collect();
if let Some(device_id) = &self.device_id {
scopes.push(device_id.to_string());
}
f.write_str(&scopes.join(" "))
}
}
@@ -126,7 +107,7 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl RawScopes {
#[allow(clippy::trivial_regex)]
pub fn to_scopes(&self) -> Result<HashSet<RequestedScope>, String> {
pub fn to_scopes(&self) -> Result<RequestedScopes, String> {
let client_api_token_regex =
regex!(r"urn:matrix:(client|org.matrix.msc2967.client):api:\*");
let device_token_regex =
@@ -134,17 +115,19 @@ pub fn to_scopes(&self) -> Result<HashSet<RequestedScope>, String> {
let server_administration_regex =
regex!(r"urn:matrix:client:cc.c10y.msc4484.server_administration");
let mut scopes = HashSet::new();
let mut scopes = RequestedScopes::default();
for token in self.0.split(' ') {
let scope_was_new = {
if client_api_token_regex.is_match(token) {
scopes.insert(RequestedScope::ClientApi)
scopes.scopes.insert(OAuthClientScope::ApiFullAccess)
} else if server_administration_regex.is_match(token) {
scopes.insert(RequestedScope::ServerAdministration)
scopes.scopes.insert(OAuthClientScope::ServerAdministration)
} else if let Some(captures) = device_token_regex.captures(token) {
scopes
.insert(RequestedScope::Device(captures.get(2).unwrap().as_str().into()))
.device_id
.replace(captures.get(2).unwrap().as_str().into())
.is_none()
} else {
continue;
}
+93 -60
View File
@@ -1,5 +1,5 @@
use std::{
collections::{BTreeSet, HashMap, HashSet},
collections::{BTreeSet, HashMap},
sync::{Arc, Mutex},
time::{Duration, SystemTime},
};
@@ -23,7 +23,7 @@
client_metadata::{ApplicationType, ClientMetadata, GrantType, ResponseType},
grant::{
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod,
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, RequestedScope,
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, RequestedScopes,
ResponseMode, TokenRequest, TokenRequestType, TokenResponse, TokenType,
},
},
@@ -56,12 +56,21 @@ struct Services {
#[derive(Debug, Deserialize, Serialize)]
pub struct SessionInfo {
pub client_id: String,
// Ignore unknown scopes, old databases might have a device scope in here
#[serde(deserialize_with = "client_metadata::btreeset_skip_err")]
pub scopes: BTreeSet<OAuthClientScope>,
scopes: BTreeSet<OAuthClientScope>,
#[serde(default)]
temporary_scopes: BTreeSet<OAuthClientScope>,
#[serde(default)]
current_refresh_token: String,
}
impl SessionInfo {
#[must_use]
pub fn scopes(&self) -> BTreeSet<OAuthClientScope> {
self.scopes.union(&self.temporary_scopes).cloned().collect()
}
}
#[derive(Debug, Deserialize, Serialize)]
struct RefreshTokenInfo {
client_id: String,
@@ -71,7 +80,7 @@ struct RefreshTokenInfo {
struct PendingAuthCodeGrant {
authorizing_user: OwnedUserId,
requested_scopes: HashSet<RequestedScope>,
requested_scopes: RequestedScopes,
client_name: Option<String>,
expected_client_id: String,
expected_redirect_uri: Url,
@@ -95,7 +104,7 @@ pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
struct PendingDeviceCodeGrant {
state: DeviceCodeGrantState,
requested_scopes: HashSet<RequestedScope>,
requested_scopes: RequestedScopes,
client_name: Option<String>,
client_id: String,
requested_at: SystemTime,
@@ -127,7 +136,7 @@ pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
pub struct DeviceCodeGrantInfo {
pub device_code: String,
pub client_metadata: ClientMetadata,
pub requested_scopes: HashSet<RequestedScope>,
pub requested_scopes: RequestedScopes,
}
/// A time-limited grant for a client to perform some sensitive action.
@@ -544,79 +553,98 @@ pub async fn revoke_token(&self, token: String) -> Result<(), OAuthError> {
async fn create_session(
&self,
authorizing_user: OwnedUserId,
requested_scopes: HashSet<RequestedScope>,
requested_scopes: RequestedScopes,
client_name: Option<String>,
client_id: String,
) -> Result<TokenResponse, OAuthError> {
self.check_requested_scopes(&authorizing_user, &requested_scopes)
self.check_requested_scopes(&authorizing_user, &requested_scopes.scopes)
.await?;
let access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let refresh_token = Self::generate_token();
let response_scope = requested_scopes.to_string();
let device_id = requested_scopes
.iter()
.find_map(|scope| {
if let RequestedScope::Device(device_id) = scope {
Some(device_id.to_owned())
} else {
None
}
})
.device_id
.ok_or_else(|| OAuthError::invalid_grant("No device ID scope supplied"))?;
if self
let session_info = if self
.services
.users
.get_device_metadata(&authorizing_user, &device_id)
.await
.is_ok()
{
return Err(OAuthError::new_static(
ErrorCode::InvalidScope,
"A device with the supplied ID already exists for this user",
));
}
// We're doing step-up authentication for an existing device
let Some(mut session_info) = self
.get_session_info_for_device(&authorizing_user, &device_id)
.await
else {
// The device ID doesn't refer to an OAuth device
return Err(OAuthError::new_static(
ErrorCode::InvalidScope,
"A device with the supplied ID already exists for this user",
));
};
let device_id = self.services
.users
.create_device(
&authorizing_user,
Some(device_id),
Some(access_token.clone()),
client_name,
None,
)
.await
// This can only panic if the authorizing user suffered a spontaneous existence
// failure during authentication, which should(?) be impossible(?)
.expect("failed to create device");
session_info.temporary_scopes = requested_scopes.scopes;
info!(
?client_id,
?authorizing_user,
?device_id,
?requested_scopes,
"Created new oauth session"
);
self.services
.users
.set_token(&authorizing_user, &device_id, access_token.clone())
.await
.expect("should be able to set access token");
self.db.userdeviceid_oauthsessioninfo.put(
(&authorizing_user, &device_id),
Json(SessionInfo {
info!(
?client_id,
?authorizing_user,
?device_id,
scopes = ?session_info.temporary_scopes,
"Granted temporary scopes to device"
);
session_info
} else {
self.services
.users
.create_device(
&authorizing_user,
Some(device_id.clone()),
Some(access_token.clone()),
client_name,
None,
)
.await
// This can only panic if the authorizing user suffered a spontaneous existence
// failure during authentication, which should(?) be impossible(?)
.expect("should be able to create device");
info!(
?client_id,
?authorizing_user,
?device_id,
scopes = ?requested_scopes.scopes,
"Created new OAuth device"
);
SessionInfo {
client_id: client_id.clone(),
current_refresh_token: refresh_token.clone(),
scopes: requested_scopes
.iter()
.filter_map(RequestedScope::as_client_scope)
.collect(),
}),
);
current_refresh_token: Self::generate_token(),
scopes: requested_scopes.scopes,
temporary_scopes: BTreeSet::new(),
}
};
let refresh_token = session_info.current_refresh_token.clone();
self.db
.userdeviceid_oauthsessioninfo
.put((&authorizing_user, &device_id), Json(session_info));
self.db.refreshtoken_refreshtokeninfo.raw_put(
&refresh_token,
Json(RefreshTokenInfo {
client_id: client_id.clone(),
user_id: authorizing_user.clone(),
client_id,
user_id: authorizing_user,
device_id,
}),
);
@@ -625,7 +653,7 @@ async fn create_session(
access_token: access_token.into_token(),
token_type: TokenType::Bearer,
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
scope: requested_scopes.iter().join(" "),
scope: response_scope,
refresh_token,
})
}
@@ -658,9 +686,12 @@ async fn refresh_session(
assert_eq!(&client_id, &session_info.client_id, "session info client id mismatch");
// TODO: this doesn't include the device ID "scope", do any clients rely
// on that?
let scope = session_info.scopes.iter().join(" ");
let new_access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let new_refresh_token = Self::generate_token();
let scope = session_info.scopes.iter().join(" ");
session_info
.current_refresh_token
.clone_from(&new_refresh_token);
@@ -675,6 +706,8 @@ async fn refresh_session(
.await
.expect("should be able to set token");
session_info.temporary_scopes.clear();
self.db.userdeviceid_oauthsessioninfo.put(
(&refresh_token_info.user_id, &refresh_token_info.device_id),
Json(session_info),
@@ -712,9 +745,9 @@ pub async fn remove_session(&self, user_id: &UserId, device_id: &DeviceId) {
async fn check_requested_scopes(
&self,
authorizing_user: &UserId,
requested_scopes: &HashSet<RequestedScope>,
scopes: &BTreeSet<OAuthClientScope>,
) -> Result<(), OAuthError> {
if requested_scopes.contains(&RequestedScope::ServerAdministration)
if scopes.contains(&OAuthClientScope::ServerAdministration)
&& !self.services.admin.user_is_admin(authorizing_user).await
{
return Err(OAuthError::new(
+39 -13
View File
@@ -5,8 +5,9 @@
routing::on,
};
use conduwuit_service::oauth::{
SessionInfo,
client_metadata::ClientMetadata,
grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt, RequestedScope},
grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt, RequestedScopes},
};
use ruma::{OwnedUserId, api::OAuthClientScope};
use serde::{Deserialize, de::IgnoredAny};
@@ -37,6 +38,7 @@ struct Grant use "grant.html.j2" {
user_avatar: Avatar,
client_metadata: ClientMetadata,
scopes: ClientScopes,
existing_session_info: Option<SessionInfo>,
device_code: Option<String>
}
}
@@ -102,13 +104,23 @@ async fn route_authorization_code(
return Err(WebError::BadRequest("Invalid client ID".to_owned()));
};
let scopes = query
.scope
.to_scopes()
.map_err(WebError::BadRequest)?
.iter()
.filter_map(RequestedScope::as_client_scope)
.collect();
let RequestedScopes { device_id, mut scopes } =
query.scope.to_scopes().map_err(WebError::BadRequest)?;
let existing_session_info = if let Some(device_id) = &device_id {
services
.oauth
.get_session_info_for_device(&user_id, device_id)
.await
} else {
None
};
if let Some(existing_session_info) = &existing_session_info {
// For step-up auth, don't show scopes that the device already has
let existing_scopes = existing_session_info.scopes();
scopes.retain(|scope| !existing_scopes.contains(scope));
}
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
@@ -124,6 +136,7 @@ async fn route_authorization_code(
user_avatar,
client,
ClientScopes { scopes },
existing_session_info,
None,
))
}
@@ -189,11 +202,23 @@ async fn route_device_code(
));
};
let scopes = grant_info
.requested_scopes
.iter()
.filter_map(RequestedScope::as_client_scope)
.collect();
let RequestedScopes { device_id, mut scopes } = grant_info.requested_scopes;
let existing_session_info = if let Some(device_id) = &device_id {
services
.oauth
.get_session_info_for_device(&user_id, device_id)
.await
} else {
None
};
if let Some(existing_session_info) = &existing_session_info {
// For step-up auth, don't show scopes that the device already
// has
let existing_scopes = existing_session_info.scopes();
scopes.retain(|scope| !existing_scopes.contains(scope));
}
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
@@ -209,6 +234,7 @@ async fn route_device_code(
user_avatar,
grant_info.client_metadata,
ClientScopes { scopes },
existing_session_info,
Some(grant_info.device_code),
))
},
-1
View File
@@ -8,7 +8,6 @@
margin-inline: 1em;
color: var(--secondary);
font-size: x-large;
font-weight: bold;
user-select: none;
}
}
@@ -4,7 +4,7 @@
{% when OAuthClientScope::ApiFullAccess %}
<li>Access and control all features of your Matrix account</li>
{% when OAuthClientScope::ServerAdministration %}
<li><em class="danger">Access your homeserver's administrator tools</em></li>
<li>⚠️ Access your homeserver's administrator tools</li>
{% when _ %}
<li>missingno</li>
{% endmatch %}
+2 -2
View File
@@ -26,7 +26,7 @@ Device information
<p>
{% if let Some((_, session_info)) = client_metadata %}
This device has permission to:
{{ ClientScopes { scopes: session_info.scopes.clone() } }}
{{ ClientScopes { scopes: session_info.scopes() } }}
{% else if device_card.dehydrated %}
This is your dehydrated device. It saves encryption keys for you while you're offline.
{% else %}
@@ -45,7 +45,7 @@ Device information
</p>
<p>
Scopes: <code>
{% for scope in session_info.scopes %}
{% for scope in session_info.scopes() %}
{{ scope | safe }}
{% endfor %}
</code>
+10 -4
View File
@@ -27,10 +27,16 @@ Authorize client
</div>
<p>
<b>{{ client_name }}</b> (<a href="{{ client_metadata.client_uri }}">{{ client_metadata.client_uri.domain().unwrap() }}</a>) would like
your permission to:
{{ scopes }}
<b>{{ client_name }}</b> (<a href="{{ client_metadata.client_uri }}">{{ client_metadata.client_uri.domain().unwrap() }}</a>)
{% if let Some(existing_session_info) = existing_session_info %}
would like temporary additional permissions to:
{{ scopes }}
{{ client_name }} already has permission to:
{{ ClientScopes { scopes: existing_session_info.scopes() } }}
{% else %}
would like your permission to:
{{ scopes }}
{% endif %}
</p>
{% if scopes.scopes.contains(&OAuthClientScope::ServerAdministration) %}