mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-10-05 03:17:56 +00:00
feat: Add support for MSC4363
This commit is contained in:
Generated
+23
-24
@@ -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
@@ -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
@@ -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 })
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
},
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) %}
|
||||
|
||||
Reference in New Issue
Block a user