Skip to content

Commit 1632b16

Browse files
committed
Make the introspection endpoint normalise stable and unstable scopes
1 parent 6d2dd06 commit 1632b16

File tree

3 files changed

+56
-25
lines changed

3 files changed

+56
-25
lines changed

crates/handlers/src/graphql/query/session.rs

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ use mas_storage::{
1111
compat::{CompatSessionFilter, CompatSessionRepository},
1212
oauth2::OAuth2SessionFilter,
1313
};
14-
use oauth2_types::scope::Scope;
1514

1615
use crate::graphql::{
1716
UserId,
@@ -77,20 +76,11 @@ impl SessionQuery {
7776
))));
7877
}
7978

80-
// Then, try to find an OAuth 2.0 session. Because we don't have any dedicated
81-
// device column, we're looking up using the device scope.
82-
// All device IDs can't necessarily be encoded as a scope. If it's not the case,
83-
// we'll skip looking for OAuth 2.0 sessions.
84-
let Ok(scope_token) = device.to_scope_token() else {
85-
repo.cancel().await?;
86-
87-
return Ok(None);
88-
};
89-
let scope = Scope::from_iter([scope_token]);
79+
// Then, try to find an OAuth 2.0 session.
9080
let filter = OAuth2SessionFilter::new()
9181
.for_user(&user)
9282
.active_only()
93-
.with_scope(&scope);
83+
.for_device(&device);
9484
let sessions = repo.oauth2_session().list(filter, pagination).await?;
9585

9686
// It's possible to have multiple active OAuth 2.0 sessions. For now, we just

crates/handlers/src/oauth2/introspection.rs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// SPDX-License-Identifier: AGPL-3.0-only
55
// Please see LICENSE in the repository root for full details.
66

7-
use std::sync::LazyLock;
7+
use std::{collections::BTreeSet, sync::LazyLock};
88

99
use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse};
1010
use hyper::{HeaderMap, StatusCode};
@@ -24,7 +24,7 @@ use mas_storage::{
2424
use oauth2_types::{
2525
errors::{ClientError, ClientErrorCode},
2626
requests::{IntrospectionRequest, IntrospectionResponse},
27-
scope::ScopeToken,
27+
scope::{Scope, ScopeToken},
2828
};
2929
use opentelemetry::{Key, KeyValue, metrics::Counter};
3030
use thiserror::Error;
@@ -190,9 +190,33 @@ const INACTIVE: IntrospectionResponse = IntrospectionResponse {
190190
device_id: None,
191191
};
192192

193-
const API_SCOPE: ScopeToken = ScopeToken::from_static("urn:matrix:org.matrix.msc2967.client:api:*");
193+
const UNSTABLE_API_SCOPE: ScopeToken =
194+
ScopeToken::from_static("urn:matrix:org.matrix.msc2967.client:api:*");
195+
const STABLE_API_SCOPE: ScopeToken = ScopeToken::from_static("urn:matrix:client:api:*");
194196
const SYNAPSE_ADMIN_SCOPE: ScopeToken = ScopeToken::from_static("urn:synapse:admin:*");
195197

198+
/// Normalize a scope by adding the stable and unstable API scopes equivalents
199+
/// if missing
200+
fn normalize_scope(mut scope: Scope) -> Scope {
201+
// Here we abuse the fact that the scope is a BTreeSet to not care about
202+
// duplicates
203+
let mut to_add = BTreeSet::new();
204+
for token in &*scope {
205+
if token == &STABLE_API_SCOPE {
206+
to_add.insert(UNSTABLE_API_SCOPE);
207+
} else if token == &UNSTABLE_API_SCOPE {
208+
to_add.insert(STABLE_API_SCOPE);
209+
} else if let Some(device) = Device::from_scope_token(token) {
210+
let tokens = device
211+
.to_scope_token()
212+
.expect("from/to scope token rountrip should never fail");
213+
to_add.extend(tokens);
214+
}
215+
}
216+
scope.append(&mut to_add);
217+
scope
218+
}
219+
196220
#[tracing::instrument(
197221
name = "handlers.oauth2.introspection.post",
198222
fields(client.id = client_authorization.client_id()),
@@ -311,9 +335,11 @@ pub(crate) async fn post(
311335
],
312336
);
313337

338+
let scope = normalize_scope(session.scope);
339+
314340
IntrospectionResponse {
315341
active: true,
316-
scope: Some(session.scope),
342+
scope: Some(scope),
317343
client_id: Some(session.client_id.to_string()),
318344
username,
319345
token_type: Some(OAuthTokenTypeHint::AccessToken),
@@ -382,9 +408,11 @@ pub(crate) async fn post(
382408
],
383409
);
384410

411+
let scope = normalize_scope(session.scope);
412+
385413
IntrospectionResponse {
386414
active: true,
387-
scope: Some(session.scope),
415+
scope: Some(scope),
388416
client_id: Some(session.client_id.to_string()),
389417
username,
390418
token_type: Some(OAuthTokenTypeHint::RefreshToken),
@@ -446,9 +474,9 @@ pub(crate) async fn post(
446474
.transpose()?
447475
};
448476

449-
let scope = [API_SCOPE]
477+
let scope = [STABLE_API_SCOPE, UNSTABLE_API_SCOPE]
450478
.into_iter()
451-
.chain(device_scope_opt)
479+
.chain(device_scope_opt.into_iter().flatten())
452480
.chain(synapse_admin_scope_opt)
453481
.collect();
454482

@@ -530,9 +558,9 @@ pub(crate) async fn post(
530558
.transpose()?
531559
};
532560

533-
let scope = [API_SCOPE]
561+
let scope = [STABLE_API_SCOPE, UNSTABLE_API_SCOPE]
534562
.into_iter()
535-
.chain(device_scope_opt)
563+
.chain(device_scope_opt.into_iter().flatten())
536564
.chain(synapse_admin_scope_opt)
537565
.collect();
538566

@@ -879,7 +907,7 @@ mod tests {
879907
let refresh_token = response["refresh_token"].as_str().unwrap();
880908
let device_id = response["device_id"].as_str().unwrap();
881909
let expected_scope: Scope =
882-
format!("urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{device_id}")
910+
format!("urn:matrix:org.matrix.msc2967.client:api:* urn:matrix:org.matrix.msc2967.client:device:{device_id} urn:matrix:client:api:* urn:matrix:client:device:{device_id}")
883911
.parse()
884912
.unwrap();
885913

@@ -912,7 +940,7 @@ mod tests {
912940
assert_eq!(response.token_type, Some(OAuthTokenTypeHint::AccessToken));
913941
assert_eq!(
914942
response.scope.map(|s| s.to_string()),
915-
Some("urn:matrix:org.matrix.msc2967.client:api:*".to_owned())
943+
Some("urn:matrix:client:api:* urn:matrix:org.matrix.msc2967.client:api:*".to_owned())
916944
);
917945
assert_eq!(response.device_id.as_deref(), Some(device_id));
918946

crates/oauth2-types/src/scope.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@
1010
1111
#![allow(clippy::module_name_repetitions)]
1212

13-
use std::{borrow::Cow, collections::BTreeSet, iter::FromIterator, ops::Deref, str::FromStr};
13+
use std::{
14+
borrow::Cow,
15+
collections::BTreeSet,
16+
iter::FromIterator,
17+
ops::{Deref, DerefMut},
18+
str::FromStr,
19+
};
1420

1521
use serde::{Deserialize, Serialize};
1622
use thiserror::Error;
@@ -121,6 +127,12 @@ impl Deref for Scope {
121127
}
122128
}
123129

130+
impl DerefMut for Scope {
131+
fn deref_mut(&mut self) -> &mut Self::Target {
132+
&mut self.0
133+
}
134+
}
135+
124136
impl FromStr for Scope {
125137
type Err = InvalidScope;
126138

@@ -248,6 +260,7 @@ mod tests {
248260
);
249261

250262
assert!(Scope::from_str("http://example.com").is_ok());
251-
assert!(Scope::from_str("urn:matrix:org.matrix.msc2967.client:*").is_ok());
263+
assert!(Scope::from_str("urn:matrix:client:api:*").is_ok());
264+
assert!(Scope::from_str("urn:matrix:org.matrix.msc2967.client:api:*").is_ok());
252265
}
253266
}

0 commit comments

Comments
 (0)