mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-08-22 12:00:38 +00:00
Include the browser session ID as sid in issued id_tokens
Persist resolved target user and user session on authorization grants
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -164,6 +165,15 @@ pub struct AuthorizationGrant {
|
||||
/// Raw query parameters from the downstream authorization request, used
|
||||
/// to template the parameters forwarded to the upstream provider.
|
||||
pub raw_parameters: BTreeMap<String, String>,
|
||||
/// The user resolved from a valid `id_token_hint`, if one was given and
|
||||
/// verified at authorize time. The raw hint itself stays in
|
||||
/// [`Self::raw_parameters`]; this is the resolved outcome.
|
||||
pub target_user_id: Option<Ulid>,
|
||||
/// The browser session resolved from the `sid` claim of a valid
|
||||
/// `id_token_hint`, if present and still existing at authorize time. May
|
||||
/// dangle after the session is reaped by retention;
|
||||
/// [`Self::target_user_id`] is the stable anchor.
|
||||
pub target_user_session_id: Option<Ulid>,
|
||||
}
|
||||
|
||||
impl std::ops::Deref for AuthorizationGrant {
|
||||
@@ -243,6 +253,8 @@ impl AuthorizationGrant {
|
||||
login_hint: Some(String::from("mxid:@example-user:example.com")),
|
||||
locale: Some(String::from("fr")),
|
||||
raw_parameters: BTreeMap::new(),
|
||||
target_user_id: None,
|
||||
target_user_session_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +267,8 @@ pub(crate) async fn get(
|
||||
params.auth.login_hint,
|
||||
Some(locale.to_string()),
|
||||
raw_parameters,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
let continue_grant = PostAuthAction::continue_grant(grant.id);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -126,6 +127,7 @@ pub(crate) async fn get(
|
||||
let claims_supported = Some(vec![
|
||||
"iss".to_owned(),
|
||||
"sub".to_owned(),
|
||||
"sid".to_owned(),
|
||||
"aud".to_owned(),
|
||||
"iat".to_owned(),
|
||||
"exp".to_owned(),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// Copyright 2025, 2026 Element Creations Ltd.
|
||||
// Copyright 2024, 2025 New Vector Ltd.
|
||||
// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
@@ -59,6 +60,7 @@ pub(crate) fn generate_id_token(
|
||||
let now = clock.now();
|
||||
claims::ISS.insert(&mut claims, url_builder.oidc_issuer().to_string())?;
|
||||
claims::SUB.insert(&mut claims, &browser_session.user.sub)?;
|
||||
claims::SID.insert(&mut claims, browser_session.id.to_string())?;
|
||||
claims::AUD.insert(&mut claims, client.client_id.clone())?;
|
||||
claims::IAT.insert(&mut claims, now)?;
|
||||
claims::EXP.insert(&mut claims, now + Duration::try_hours(1).unwrap())?;
|
||||
|
||||
@@ -1122,6 +1122,8 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
std::collections::BTreeMap::new(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1147,11 +1149,24 @@ mod tests {
|
||||
let response = state.request(request).await;
|
||||
response.assert_status(StatusCode::OK);
|
||||
|
||||
let AccessTokenResponse { access_token, .. } = response.json();
|
||||
let AccessTokenResponse {
|
||||
access_token,
|
||||
id_token,
|
||||
..
|
||||
} = response.json();
|
||||
|
||||
// Check that the token is valid
|
||||
assert!(state.is_access_token_valid(&access_token).await);
|
||||
|
||||
// We asked for the openid scope, so we should have an ID token, and it
|
||||
// should carry a `sid` claim equal to the browser session ID.
|
||||
let id_token = id_token.expect("an ID token should be present for the openid scope");
|
||||
let id_token: mas_jose::jwt::Jwt<std::collections::HashMap<String, serde_json::Value>> =
|
||||
id_token.as_str().try_into().unwrap();
|
||||
let (_, mut claims) = id_token.into_parts();
|
||||
let sid = mas_jose::claims::SID.extract_required(&mut claims).unwrap();
|
||||
assert_eq!(sid, browser_session.id.to_string());
|
||||
|
||||
// Exchange it again, this it should fail
|
||||
let request =
|
||||
Request::post(mas_router::OAuth2TokenEndpoint::PATH).form(serde_json::json!({
|
||||
@@ -1211,6 +1226,8 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
std::collections::BTreeMap::new(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , raw_parameters AS \"raw_parameters: Json<BTreeMap<String, String>>\"\n , user_session_id\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , raw_parameters AS \"raw_parameters: Json<BTreeMap<String, String>>\"\n , target_user_id\n , target_user_session_id\n , user_session_id\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -100,11 +100,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "user_session_id",
|
||||
"name": "target_user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "target_user_session_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "user_session_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "oauth2_session_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -135,8 +145,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "008ef8a2092fd2424ab6215bcad376b72f26b723dc029624396bb2030e53907c"
|
||||
"hash": "4040e4d3cecfb2b25e0c3d141df525da902344c79b50e8dd205b1fffe58665d9"
|
||||
}
|
||||
+4
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n locale,\n raw_parameters,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n ",
|
||||
"query": "\n INSERT INTO oauth2_authorization_grants (\n oauth2_authorization_grant_id,\n oauth2_client_id,\n redirect_uri,\n scope,\n state,\n nonce,\n response_mode,\n code_challenge,\n code_challenge_method,\n response_type_code,\n response_type_id_token,\n authorization_code,\n login_hint,\n locale,\n raw_parameters,\n target_user_id,\n target_user_session_id,\n created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -20,10 +20,12 @@
|
||||
"Text",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Uuid",
|
||||
"Uuid",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "041c4ddff9b40ff5ba16c9aa1dd9c721998de6e5798e4423df9063519ee5ac4d"
|
||||
"hash": "4f152ad116f636d513373e07ecddb9a2470a8f1c37401e27192bcf9927b12979"
|
||||
}
|
||||
+15
-3
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , raw_parameters AS \"raw_parameters: Json<BTreeMap<String, String>>\"\n , user_session_id\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ",
|
||||
"query": "\n SELECT oauth2_authorization_grant_id\n , created_at\n , cancelled_at\n , fulfilled_at\n , exchanged_at\n , scope\n , state\n , redirect_uri\n , response_mode\n , nonce\n , oauth2_client_id\n , authorization_code\n , response_type_code\n , response_type_id_token\n , code_challenge\n , code_challenge_method\n , login_hint\n , locale\n , raw_parameters AS \"raw_parameters: Json<BTreeMap<String, String>>\"\n , target_user_id\n , target_user_session_id\n , user_session_id\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -100,11 +100,21 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"name": "user_session_id",
|
||||
"name": "target_user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"name": "target_user_session_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"name": "user_session_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"name": "oauth2_session_id",
|
||||
"type_info": "Uuid"
|
||||
}
|
||||
@@ -135,8 +145,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "2921f8cb19dedfc5524298ba4f42580244a4e5611d7aa8ade990bd5b2230f410"
|
||||
"hash": "65e79b677464a0007cd8afe678953bbea9c52d1432815d3ff47f934d292cecf0"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
-- Copyright 2026 Element Creations Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE files in the repository root for full details.
|
||||
|
||||
-- Records the resolved outcome of verifying a hint on the authorization
|
||||
-- request: the target user and a user session.
|
||||
ALTER TABLE oauth2_authorization_grants
|
||||
ADD COLUMN target_user_id UUID
|
||||
REFERENCES users (user_id),
|
||||
ADD COLUMN target_user_session_id UUID
|
||||
REFERENCES user_sessions (user_session_id)
|
||||
ON DELETE SET NULL;
|
||||
@@ -0,0 +1,9 @@
|
||||
-- no-transaction
|
||||
-- Copyright 2026 Element Creations Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE files in the repository root for full details.
|
||||
|
||||
CREATE INDEX CONCURRENTLY IF NOT EXISTS
|
||||
oauth2_authorization_grants_target_session_fk
|
||||
ON oauth2_authorization_grants (target_user_session_id);
|
||||
@@ -11,7 +11,7 @@ use async_trait::async_trait;
|
||||
use chrono::{DateTime, Utc};
|
||||
use mas_data_model::{
|
||||
AuthorizationCode, AuthorizationGrant, AuthorizationGrantStage, BrowserSession, Client, Clock,
|
||||
Pkce, Session, UlidExt as _,
|
||||
Pkce, Session, UlidExt as _, User,
|
||||
};
|
||||
use mas_iana::oauth::PkceCodeChallengeMethod;
|
||||
use mas_storage::oauth2::OAuth2AuthorizationGrantRepository;
|
||||
@@ -57,6 +57,8 @@ struct GrantLookup {
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
raw_parameters: Option<Json<BTreeMap<String, String>>>,
|
||||
target_user_id: Option<Uuid>,
|
||||
target_user_session_id: Option<Uuid>,
|
||||
oauth2_client_id: Uuid,
|
||||
user_session_id: Option<Uuid>,
|
||||
oauth2_session_id: Option<Uuid>,
|
||||
@@ -179,6 +181,8 @@ impl TryFrom<GrantLookup> for AuthorizationGrant {
|
||||
login_hint: value.login_hint,
|
||||
locale: value.locale,
|
||||
raw_parameters: value.raw_parameters.map(|Json(x)| x).unwrap_or_default(),
|
||||
target_user_id: value.target_user_id.map(Into::into),
|
||||
target_user_session_id: value.target_user_session_id.map(Into::into),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -213,6 +217,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
raw_parameters: BTreeMap<String, String>,
|
||||
target_user: Option<&User>,
|
||||
target_user_session: Option<&BrowserSession>,
|
||||
) -> Result<AuthorizationGrant, Self::Error> {
|
||||
let code_challenge = code
|
||||
.as_ref()
|
||||
@@ -223,6 +229,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
.and_then(|c| c.pkce.as_ref())
|
||||
.map(|p| p.challenge_method.to_string());
|
||||
let code_str = code.as_ref().map(|c| &c.code);
|
||||
let target_user_id = target_user.map(|u| u.id);
|
||||
let target_user_session_id = target_user_session.map(|s| s.id);
|
||||
|
||||
let created_at = clock.now();
|
||||
let id = Ulid::from_datetime_with_rng(created_at, rng);
|
||||
@@ -246,10 +254,12 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
login_hint,
|
||||
locale,
|
||||
raw_parameters,
|
||||
target_user_id,
|
||||
target_user_session_id,
|
||||
created_at
|
||||
)
|
||||
VALUES
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
|
||||
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
|
||||
"#,
|
||||
Uuid::from(id),
|
||||
Uuid::from(client.id),
|
||||
@@ -266,6 +276,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
login_hint,
|
||||
locale,
|
||||
Json(&raw_parameters) as _,
|
||||
target_user_id.map(Uuid::from),
|
||||
target_user_session_id.map(Uuid::from),
|
||||
created_at,
|
||||
)
|
||||
.traced()
|
||||
@@ -287,6 +299,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
login_hint,
|
||||
locale,
|
||||
raw_parameters,
|
||||
target_user_id,
|
||||
target_user_session_id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -322,6 +336,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
, login_hint
|
||||
, locale
|
||||
, raw_parameters AS "raw_parameters: Json<BTreeMap<String, String>>"
|
||||
, target_user_id
|
||||
, target_user_session_id
|
||||
, user_session_id
|
||||
, oauth2_session_id
|
||||
FROM
|
||||
@@ -374,6 +390,8 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository
|
||||
, login_hint
|
||||
, locale
|
||||
, raw_parameters AS "raw_parameters: Json<BTreeMap<String, String>>"
|
||||
, target_user_id
|
||||
, target_user_session_id
|
||||
, user_session_id
|
||||
, oauth2_session_id
|
||||
FROM
|
||||
|
||||
@@ -148,6 +148,8 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
raw_parameters.clone(),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::collections::BTreeMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use mas_data_model::{
|
||||
AuthorizationCode, AuthorizationGrant, BrowserSession, Client, Clock, Session,
|
||||
AuthorizationCode, AuthorizationGrant, BrowserSession, Client, Clock, Session, User,
|
||||
};
|
||||
use oauth2_types::{requests::ResponseMode, scope::Scope};
|
||||
use rand_core::RngCore;
|
||||
@@ -49,6 +49,9 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
/// * `raw_parameters`: The raw query parameters of the authorization
|
||||
/// request, used to template the parameters forwarded to the upstream
|
||||
/// provider
|
||||
/// * `target_user`: The user resolved from a valid hint, if any
|
||||
/// * `target_user_session`: The browser session resolved from the `sid`
|
||||
/// claim of a valid hint, if any
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
@@ -69,6 +72,8 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync {
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
raw_parameters: BTreeMap<String, String>,
|
||||
target_user: Option<&User>,
|
||||
target_user_session: Option<&BrowserSession>,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
/// Lookup an authorization grant by its ID
|
||||
@@ -180,6 +185,8 @@ repository_impl!(OAuth2AuthorizationGrantRepository:
|
||||
login_hint: Option<String>,
|
||||
locale: Option<String>,
|
||||
raw_parameters: BTreeMap<String, String>,
|
||||
target_user: Option<&User>,
|
||||
target_user_session: Option<&BrowserSession>,
|
||||
) -> Result<AuthorizationGrant, Self::Error>;
|
||||
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<AuthorizationGrant>, Self::Error>;
|
||||
|
||||
@@ -788,9 +788,9 @@ impl ConsentContext {
|
||||
#[serde(tag = "grant_type")]
|
||||
enum PolicyViolationGrant {
|
||||
#[serde(rename = "authorization_code")]
|
||||
Authorization(AuthorizationGrant),
|
||||
Authorization(Box<AuthorizationGrant>),
|
||||
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
|
||||
DeviceCode(DeviceCodeGrant),
|
||||
DeviceCode(Box<DeviceCodeGrant>),
|
||||
}
|
||||
|
||||
/// Context used by the `policy_violation.html` template
|
||||
@@ -853,14 +853,14 @@ impl PolicyViolationContext {
|
||||
/// Constructs a context for the policy violation page for an authorization
|
||||
/// grant
|
||||
#[must_use]
|
||||
pub const fn for_authorization_grant(
|
||||
pub fn for_authorization_grant(
|
||||
grant: AuthorizationGrant,
|
||||
client: Client,
|
||||
violations: Vec<Violation>,
|
||||
) -> Self {
|
||||
let action = PostAuthAction::continue_grant(grant.id);
|
||||
Self {
|
||||
grant: PolicyViolationGrant::Authorization(grant),
|
||||
grant: PolicyViolationGrant::Authorization(Box::new(grant)),
|
||||
client,
|
||||
action,
|
||||
violations,
|
||||
@@ -870,14 +870,14 @@ impl PolicyViolationContext {
|
||||
/// Constructs a context for the policy violation page for a device code
|
||||
/// grant
|
||||
#[must_use]
|
||||
pub const fn for_device_code_grant(
|
||||
pub fn for_device_code_grant(
|
||||
grant: DeviceCodeGrant,
|
||||
client: Client,
|
||||
violations: Vec<Violation>,
|
||||
) -> Self {
|
||||
let action = PostAuthAction::continue_device_code_grant(grant.id);
|
||||
Self {
|
||||
grant: PolicyViolationGrant::DeviceCode(grant),
|
||||
grant: PolicyViolationGrant::DeviceCode(Box::new(grant)),
|
||||
client,
|
||||
action,
|
||||
violations,
|
||||
|
||||
Reference in New Issue
Block a user