diff --git a/crates/data-model/src/oauth2/authorization_grant.rs b/crates/data-model/src/oauth2/authorization_grant.rs index 2ad0488ce..36361d7c1 100644 --- a/crates/data-model/src/oauth2/authorization_grant.rs +++ b/crates/data-model/src/oauth2/authorization_grant.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +use std::collections::BTreeMap; + use chrono::{DateTime, Utc}; use mas_iana::oauth::PkceCodeChallengeMethod; use oauth2_types::{ @@ -155,6 +157,9 @@ pub struct AuthorizationGrant { pub created_at: DateTime, pub login_hint: Option, pub locale: Option, + /// Raw query parameters from the downstream authorization request, used + /// to template the parameters forwarded to the upstream provider. + pub raw_parameters: BTreeMap, } impl std::ops::Deref for AuthorizationGrant { @@ -229,6 +234,7 @@ impl AuthorizationGrant { created_at: now, login_hint: Some(String::from("mxid:@example-user:example.com")), locale: Some(String::from("fr")), + raw_parameters: BTreeMap::new(), } } } diff --git a/crates/handlers/src/oauth2/authorization/mod.rs b/crates/handlers/src/oauth2/authorization/mod.rs index a3988b0ce..19af0aa4f 100644 --- a/crates/handlers/src/oauth2/authorization/mod.rs +++ b/crates/handlers/src/oauth2/authorization/mod.rs @@ -260,6 +260,7 @@ pub(crate) async fn get( response_type.has_id_token(), params.auth.login_hint, Some(locale.to_string()), + std::collections::BTreeMap::new(), ) .await?; let continue_grant = PostAuthAction::continue_grant(grant.id); diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 49dd7cb68..96cc9a4fd 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -1073,6 +1073,7 @@ mod tests { false, None, None, + std::collections::BTreeMap::new(), ) .await .unwrap(); @@ -1173,6 +1174,7 @@ mod tests { false, None, None, + std::collections::BTreeMap::new(), ) .await .unwrap(); diff --git a/crates/storage-pg/.sqlx/query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json b/crates/storage-pg/.sqlx/query-041c4ddff9b40ff5ba16c9aa1dd9c721998de6e5798e4423df9063519ee5ac4d.json similarity index 73% rename from crates/storage-pg/.sqlx/query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json rename to crates/storage-pg/.sqlx/query-041c4ddff9b40ff5ba16c9aa1dd9c721998de6e5798e4423df9063519ee5ac4d.json index 22c3bc0eb..37d3db2ab 100644 --- a/crates/storage-pg/.sqlx/query-7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567.json +++ b/crates/storage-pg/.sqlx/query-041c4ddff9b40ff5ba16c9aa1dd9c721998de6e5798e4423df9063519ee5ac4d.json @@ -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 created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)\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 created_at\n )\n VALUES\n ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)\n ", "describe": { "columns": [], "parameters": { @@ -19,10 +19,11 @@ "Text", "Text", "Text", + "Jsonb", "Timestamptz" ] }, "nullable": [] }, - "hash": "7a0641df5058927c5cd67d4cdaa59fe609112afbabcbfcc0e7f96c1e531b6567" + "hash": "041c4ddff9b40ff5ba16c9aa1dd9c721998de6e5798e4423df9063519ee5ac4d" } diff --git a/crates/storage-pg/.sqlx/query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json b/crates/storage-pg/.sqlx/query-2bdf2e1ed7e207d0165b59153b274ea2941055fdbb6b5a6989b229a2fd68925c.json similarity index 85% rename from crates/storage-pg/.sqlx/query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json rename to crates/storage-pg/.sqlx/query-2bdf2e1ed7e207d0165b59153b274ea2941055fdbb6b5a6989b229a2fd68925c.json index 20cd2c704..b47232a8f 100644 --- a/crates/storage-pg/.sqlx/query-c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e.json +++ b/crates/storage-pg/.sqlx/query-2bdf2e1ed7e207d0165b59153b274ea2941055fdbb6b5a6989b229a2fd68925c.json @@ -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 , 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>\"\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE oauth2_authorization_grant_id = $1\n ", "describe": { "columns": [ { @@ -95,6 +95,11 @@ }, { "ordinal": 18, + "name": "raw_parameters: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 19, "name": "oauth2_session_id", "type_info": "Uuid" } @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "c960f4f5571ee68816c49898125979f3c78c2caca52cb4b8dc9880e669a1f23e" + "hash": "2bdf2e1ed7e207d0165b59153b274ea2941055fdbb6b5a6989b229a2fd68925c" } diff --git a/crates/storage-pg/.sqlx/query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json b/crates/storage-pg/.sqlx/query-47ddb26cbdb3410ef80020c7835ecad9b9ca26452915553dfd6c37b3dccae710.json similarity index 86% rename from crates/storage-pg/.sqlx/query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json rename to crates/storage-pg/.sqlx/query-47ddb26cbdb3410ef80020c7835ecad9b9ca26452915553dfd6c37b3dccae710.json index 0a5d83f0a..fe67a5b40 100644 --- a/crates/storage-pg/.sqlx/query-8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035.json +++ b/crates/storage-pg/.sqlx/query-47ddb26cbdb3410ef80020c7835ecad9b9ca26452915553dfd6c37b3dccae710.json @@ -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 , 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>\"\n , oauth2_session_id\n FROM\n oauth2_authorization_grants\n\n WHERE authorization_code = $1\n ", "describe": { "columns": [ { @@ -95,6 +95,11 @@ }, { "ordinal": 18, + "name": "raw_parameters: Json>", + "type_info": "Jsonb" + }, + { + "ordinal": 19, "name": "oauth2_session_id", "type_info": "Uuid" } @@ -123,8 +128,9 @@ true, true, true, + true, true ] }, - "hash": "8ef27901b96b73826a431ad6c5fabecc18c36d8cdba8db3b47953855fa5c9035" + "hash": "47ddb26cbdb3410ef80020c7835ecad9b9ca26452915553dfd6c37b3dccae710" } diff --git a/crates/storage-pg/migrations/20260513103411_oauth2_authorization_grant_raw_parameters.sql b/crates/storage-pg/migrations/20260513103411_oauth2_authorization_grant_raw_parameters.sql new file mode 100644 index 000000000..0d978cf12 --- /dev/null +++ b/crates/storage-pg/migrations/20260513103411_oauth2_authorization_grant_raw_parameters.sql @@ -0,0 +1,10 @@ +-- Copyright 2026 New Vector Ltd. +-- +-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +-- Please see LICENSE files in the repository root for full details. + +-- Capture the raw query parameters from the downstream OAuth2 authorization +-- request, so the upstream OAuth2 provider authorization handler can +-- reference them from templated `additional_authorization_parameters`. +ALTER TABLE "oauth2_authorization_grants" + ADD COLUMN "raw_parameters" JSONB; diff --git a/crates/storage-pg/src/oauth2/authorization_grant.rs b/crates/storage-pg/src/oauth2/authorization_grant.rs index 827a930ab..30f6a8ae0 100644 --- a/crates/storage-pg/src/oauth2/authorization_grant.rs +++ b/crates/storage-pg/src/oauth2/authorization_grant.rs @@ -5,6 +5,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +use std::collections::BTreeMap; + use async_trait::async_trait; use chrono::{DateTime, Utc}; use mas_data_model::{ @@ -14,7 +16,7 @@ use mas_iana::oauth::PkceCodeChallengeMethod; use mas_storage::oauth2::OAuth2AuthorizationGrantRepository; use oauth2_types::{requests::ResponseMode, scope::Scope}; use rand::RngCore; -use sqlx::PgConnection; +use sqlx::{PgConnection, types::Json}; use ulid::Ulid; use url::Url; use uuid::Uuid; @@ -54,6 +56,7 @@ struct GrantLookup { code_challenge_method: Option, login_hint: Option, locale: Option, + raw_parameters: Option>>, oauth2_client_id: Uuid, oauth2_session_id: Option, } @@ -164,6 +167,7 @@ impl TryFrom for AuthorizationGrant { response_type_id_token: value.response_type_id_token, login_hint: value.login_hint, locale: value.locale, + raw_parameters: value.raw_parameters.map(|Json(x)| x).unwrap_or_default(), }) } } @@ -197,6 +201,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository response_type_id_token: bool, login_hint: Option, locale: Option, + raw_parameters: BTreeMap, ) -> Result { let code_challenge = code .as_ref() @@ -229,10 +234,11 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository authorization_code, login_hint, locale, + raw_parameters, created_at ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) "#, Uuid::from(id), Uuid::from(client.id), @@ -248,6 +254,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository code_str, login_hint, locale, + Json(&raw_parameters) as _, created_at, ) .traced() @@ -268,6 +275,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository response_type_id_token, login_hint, locale, + raw_parameters, }) } @@ -302,6 +310,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository , code_challenge_method , login_hint , locale + , raw_parameters AS "raw_parameters: Json>" , oauth2_session_id FROM oauth2_authorization_grants @@ -352,6 +361,7 @@ impl OAuth2AuthorizationGrantRepository for PgOAuth2AuthorizationGrantRepository , code_challenge_method , login_hint , locale + , raw_parameters AS "raw_parameters: Json>" , oauth2_session_id FROM oauth2_authorization_grants diff --git a/crates/storage-pg/src/oauth2/mod.rs b/crates/storage-pg/src/oauth2/mod.rs index bf741b5f2..de40fe863 100644 --- a/crates/storage-pg/src/oauth2/mod.rs +++ b/crates/storage-pg/src/oauth2/mod.rs @@ -120,6 +120,10 @@ mod tests { assert_eq!(grant, None); // Create an authorization grant + let raw_parameters = std::collections::BTreeMap::from([ + ("client_id".to_owned(), "client".to_owned()), + ("foo".to_owned(), "bar".to_owned()), + ]); let grant = repo .oauth2_authorization_grant() .add( @@ -138,10 +142,12 @@ mod tests { true, None, None, + raw_parameters.clone(), ) .await .unwrap(); assert!(grant.is_pending()); + assert_eq!(grant.raw_parameters, raw_parameters); // Lookup the same grant by id let grant_lookup = repo diff --git a/crates/storage/src/oauth2/authorization_grant.rs b/crates/storage/src/oauth2/authorization_grant.rs index c0f1030e3..89e5fd600 100644 --- a/crates/storage/src/oauth2/authorization_grant.rs +++ b/crates/storage/src/oauth2/authorization_grant.rs @@ -5,6 +5,8 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. +use std::collections::BTreeMap; + use async_trait::async_trait; use mas_data_model::{AuthorizationCode, AuthorizationGrant, Client, Clock, Session}; use oauth2_types::{requests::ResponseMode, scope::Scope}; @@ -42,6 +44,9 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { /// * `login_hint`: The `login_hint` the client sent, if set /// * `locale`: The locale the detected when the user asked for the /// authorization grant + /// * `raw_parameters`: The raw query parameters of the authorization + /// request, used to template the parameters forwarded to the upstream + /// provider /// /// # Errors /// @@ -61,6 +66,7 @@ pub trait OAuth2AuthorizationGrantRepository: Send + Sync { response_type_id_token: bool, login_hint: Option, locale: Option, + raw_parameters: BTreeMap, ) -> Result; /// Lookup an authorization grant by its ID @@ -169,6 +175,7 @@ repository_impl!(OAuth2AuthorizationGrantRepository: response_type_id_token: bool, login_hint: Option, locale: Option, + raw_parameters: BTreeMap, ) -> Result; async fn lookup(&mut self, id: Ulid) -> Result, Self::Error>;