Record the decoded ID token claims on upstream auth sessions
This commit is contained in:
@@ -19,6 +19,7 @@ pub enum UpstreamOAuthAuthorizationSessionState {
|
||||
completed_at: DateTime<Utc>,
|
||||
link_id: Ulid,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
},
|
||||
@@ -27,6 +28,7 @@ pub enum UpstreamOAuthAuthorizationSessionState {
|
||||
consumed_at: DateTime<Utc>,
|
||||
link_id: Ulid,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
},
|
||||
@@ -35,6 +37,7 @@ pub enum UpstreamOAuthAuthorizationSessionState {
|
||||
consumed_at: Option<DateTime<Utc>>,
|
||||
unlinked_at: DateTime<Utc>,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -52,6 +55,7 @@ impl UpstreamOAuthAuthorizationSessionState {
|
||||
completed_at: DateTime<Utc>,
|
||||
link: &UpstreamOAuthLink,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
) -> Result<Self, InvalidTransitionError> {
|
||||
@@ -60,6 +64,7 @@ impl UpstreamOAuthAuthorizationSessionState {
|
||||
completed_at,
|
||||
link_id: link.id,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
}),
|
||||
@@ -83,6 +88,7 @@ impl UpstreamOAuthAuthorizationSessionState {
|
||||
completed_at,
|
||||
link_id,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
} => Ok(Self::Consumed {
|
||||
@@ -90,6 +96,7 @@ impl UpstreamOAuthAuthorizationSessionState {
|
||||
link_id,
|
||||
consumed_at,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
}),
|
||||
@@ -146,6 +153,29 @@ impl UpstreamOAuthAuthorizationSessionState {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ID token claims for the upstream OAuth 2.0 authorization
|
||||
/// session.
|
||||
///
|
||||
/// Returns `None` if the upstream OAuth 2.0 authorization session state is
|
||||
/// not [`Pending`].
|
||||
///
|
||||
/// [`Pending`]: UpstreamOAuthAuthorizationSessionState::Pending
|
||||
#[must_use]
|
||||
pub fn id_token_claims(&self) -> Option<&serde_json::Value> {
|
||||
match self {
|
||||
Self::Pending => None,
|
||||
Self::Completed {
|
||||
id_token_claims, ..
|
||||
}
|
||||
| Self::Consumed {
|
||||
id_token_claims, ..
|
||||
}
|
||||
| Self::Unlinked {
|
||||
id_token_claims, ..
|
||||
} => id_token_claims.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the extra query parameters that were sent to the upstream provider.
|
||||
///
|
||||
/// Returns `None` if the upstream OAuth 2.0 authorization session state is
|
||||
@@ -277,6 +307,7 @@ impl UpstreamOAuthAuthorizationSession {
|
||||
completed_at: DateTime<Utc>,
|
||||
link: &UpstreamOAuthLink,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
) -> Result<Self, InvalidTransitionError> {
|
||||
@@ -284,6 +315,7 @@ impl UpstreamOAuthAuthorizationSession {
|
||||
completed_at,
|
||||
link,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
)?;
|
||||
|
||||
@@ -126,7 +126,7 @@ mod tests {
|
||||
|
||||
let session = repo
|
||||
.upstream_oauth_session()
|
||||
.complete_with_link(&state.clock, session, &link, None, None, None)
|
||||
.complete_with_link(&state.clock, session, &link, None, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -312,6 +312,7 @@ pub(crate) async fn handler(
|
||||
.await?;
|
||||
|
||||
let mut jwks = None;
|
||||
let mut id_token_claims = None;
|
||||
|
||||
let mut context = AttributeMappingContext::new();
|
||||
if let Some(id_token) = token_response.id_token.as_ref() {
|
||||
@@ -337,6 +338,14 @@ pub(crate) async fn handler(
|
||||
|
||||
let (_headers, mut claims) = id_token.into_parts();
|
||||
|
||||
// Save a copy of the claims for later; the claims extract methods
|
||||
// remove them from the map, and we want to store the original claims.
|
||||
// We anyway need this to be a serde_json::Value
|
||||
id_token_claims = Some(
|
||||
serde_json::to_value(&claims)
|
||||
.expect("serializing a HashMap<String, Value> into a Value should never fail"),
|
||||
);
|
||||
|
||||
// Access token hash must match.
|
||||
mas_jose::claims::AT_HASH
|
||||
.extract_optional_with_options(
|
||||
@@ -472,6 +481,7 @@ pub(crate) async fn handler(
|
||||
session,
|
||||
&link,
|
||||
token_response.id_token,
|
||||
id_token_claims,
|
||||
params.extra_callback_parameters,
|
||||
userinfo,
|
||||
)
|
||||
|
||||
@@ -934,7 +934,7 @@ mod tests {
|
||||
..UpstreamOAuthProviderClaimsImports::default()
|
||||
};
|
||||
|
||||
let id_token = serde_json::json!({
|
||||
let id_token_claims = serde_json::json!({
|
||||
"preferred_username": "john",
|
||||
"email": "john@example.com",
|
||||
"email_verified": true,
|
||||
@@ -953,7 +953,8 @@ mod tests {
|
||||
.signing_key_for_alg(&JsonWebSignatureAlg::Rs256)
|
||||
.unwrap();
|
||||
let header = JsonWebSignatureHeader::new(JsonWebSignatureAlg::Rs256);
|
||||
let id_token = Jwt::sign_with_rng(&mut rng, header, id_token, &signer).unwrap();
|
||||
let id_token =
|
||||
Jwt::sign_with_rng(&mut rng, header, id_token_claims.clone(), &signer).unwrap();
|
||||
|
||||
// Provision a provider and a link
|
||||
let mut repo = state.repository().await.unwrap();
|
||||
@@ -1022,6 +1023,7 @@ mod tests {
|
||||
session,
|
||||
&link,
|
||||
Some(id_token.into_string()),
|
||||
Some(id_token_claims),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE upstream_oauth_authorization_sessions\n SET upstream_oauth_link_id = $1,\n completed_at = $2,\n id_token = $3,\n extra_callback_parameters = $4,\n userinfo = $5\n WHERE upstream_oauth_authorization_session_id = $6\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Timestamptz",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5f5245ace61b896f92be78ab4fef701b37c9e3c2f4a332f418b9fb2625a0fe3f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n upstream_oauth_authorization_session_id,\n upstream_oauth_provider_id,\n upstream_oauth_link_id,\n state,\n code_challenge_verifier,\n nonce,\n id_token,\n extra_callback_parameters,\n userinfo,\n created_at,\n completed_at,\n consumed_at,\n unlinked_at\n FROM upstream_oauth_authorization_sessions\n WHERE upstream_oauth_authorization_session_id = $1\n ",
|
||||
"query": "\n SELECT\n upstream_oauth_authorization_session_id,\n upstream_oauth_provider_id,\n upstream_oauth_link_id,\n state,\n code_challenge_verifier,\n nonce,\n id_token,\n id_token_claims,\n extra_callback_parameters,\n userinfo,\n created_at,\n completed_at,\n consumed_at,\n unlinked_at\n FROM upstream_oauth_authorization_sessions\n WHERE upstream_oauth_authorization_session_id = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -40,31 +40,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "extra_callback_parameters",
|
||||
"name": "id_token_claims",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "userinfo",
|
||||
"name": "extra_callback_parameters",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "userinfo",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "completed_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "consumed_at",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "unlinked_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
@@ -84,11 +89,12 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "37a124678323380357fa9d1375fd125fb35476ac3008e5adbd04a761d5edcd42"
|
||||
"hash": "e62d043f86e7232e6e9433631f8273e7ed0770c81071cf1f17516d3a45881ae9"
|
||||
}
|
||||
20
crates/storage-pg/.sqlx/query-fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50.json
generated
Normal file
20
crates/storage-pg/.sqlx/query-fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE upstream_oauth_authorization_sessions\n SET upstream_oauth_link_id = $1\n , completed_at = $2\n , id_token = $3\n , id_token_claims = $4\n , extra_callback_parameters = $5\n , userinfo = $6\n WHERE upstream_oauth_authorization_session_id = $7\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Uuid",
|
||||
"Timestamptz",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Uuid"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "fd8f3e7ff02d4d1f465aad32edcb06a842cabc787279ba7d690f69b59ad3eb50"
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
-- Copyright 2025 New Vector Ltd.
|
||||
--
|
||||
-- SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
-- Please see LICENSE in the repository root for full details.
|
||||
|
||||
-- This is the decoded claims from the ID token stored as JSONB
|
||||
ALTER TABLE upstream_oauth_authorization_sessions
|
||||
ADD COLUMN id_token_claims JSONB;
|
||||
@@ -152,7 +152,7 @@ mod tests {
|
||||
|
||||
let session = repo
|
||||
.upstream_oauth_session()
|
||||
.complete_with_link(&clock, session, &link, None, None, None)
|
||||
.complete_with_link(&clock, session, &link, None, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
// Reload the session
|
||||
|
||||
@@ -40,6 +40,7 @@ struct SessionLookup {
|
||||
code_challenge_verifier: Option<String>,
|
||||
nonce: Option<String>,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
created_at: DateTime<Utc>,
|
||||
completed_at: Option<DateTime<Utc>>,
|
||||
@@ -56,18 +57,20 @@ impl TryFrom<SessionLookup> for UpstreamOAuthAuthorizationSession {
|
||||
let state = match (
|
||||
value.upstream_oauth_link_id,
|
||||
value.id_token,
|
||||
value.id_token_claims,
|
||||
value.extra_callback_parameters,
|
||||
value.userinfo,
|
||||
value.completed_at,
|
||||
value.consumed_at,
|
||||
value.unlinked_at,
|
||||
) {
|
||||
(None, None, None, None, None, None, None) => {
|
||||
(None, None, None, None, None, None, None, None) => {
|
||||
UpstreamOAuthAuthorizationSessionState::Pending
|
||||
}
|
||||
(
|
||||
Some(link_id),
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
Some(completed_at),
|
||||
@@ -77,12 +80,14 @@ impl TryFrom<SessionLookup> for UpstreamOAuthAuthorizationSession {
|
||||
completed_at,
|
||||
link_id: link_id.into(),
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
},
|
||||
(
|
||||
Some(link_id),
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
Some(completed_at),
|
||||
@@ -92,18 +97,27 @@ impl TryFrom<SessionLookup> for UpstreamOAuthAuthorizationSession {
|
||||
completed_at,
|
||||
link_id: link_id.into(),
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
consumed_at,
|
||||
},
|
||||
(_, id_token, _, _, Some(completed_at), consumed_at, Some(unlinked_at)) => {
|
||||
UpstreamOAuthAuthorizationSessionState::Unlinked {
|
||||
completed_at,
|
||||
id_token,
|
||||
consumed_at,
|
||||
unlinked_at,
|
||||
}
|
||||
}
|
||||
(
|
||||
_,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
_,
|
||||
_,
|
||||
Some(completed_at),
|
||||
consumed_at,
|
||||
Some(unlinked_at),
|
||||
) => UpstreamOAuthAuthorizationSessionState::Unlinked {
|
||||
completed_at,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
consumed_at,
|
||||
unlinked_at,
|
||||
},
|
||||
_ => {
|
||||
return Err(DatabaseInconsistencyError::on(
|
||||
"upstream_oauth_authorization_sessions",
|
||||
@@ -152,6 +166,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> {
|
||||
code_challenge_verifier,
|
||||
nonce,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
created_at,
|
||||
@@ -253,6 +268,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> {
|
||||
upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession,
|
||||
upstream_oauth_link: &UpstreamOAuthLink,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error> {
|
||||
@@ -261,16 +277,18 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE upstream_oauth_authorization_sessions
|
||||
SET upstream_oauth_link_id = $1,
|
||||
completed_at = $2,
|
||||
id_token = $3,
|
||||
extra_callback_parameters = $4,
|
||||
userinfo = $5
|
||||
WHERE upstream_oauth_authorization_session_id = $6
|
||||
SET upstream_oauth_link_id = $1
|
||||
, completed_at = $2
|
||||
, id_token = $3
|
||||
, id_token_claims = $4
|
||||
, extra_callback_parameters = $5
|
||||
, userinfo = $6
|
||||
WHERE upstream_oauth_authorization_session_id = $7
|
||||
"#,
|
||||
Uuid::from(upstream_oauth_link.id),
|
||||
completed_at,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
Uuid::from(upstream_oauth_authorization_session.id),
|
||||
@@ -284,6 +302,7 @@ impl UpstreamOAuthSessionRepository for PgUpstreamOAuthSessionRepository<'_> {
|
||||
completed_at,
|
||||
upstream_oauth_link,
|
||||
id_token,
|
||||
id_token_claims,
|
||||
extra_callback_parameters,
|
||||
userinfo,
|
||||
)
|
||||
|
||||
@@ -74,18 +74,23 @@ pub trait UpstreamOAuthSessionRepository: Send + Sync {
|
||||
/// * `upstream_oauth_link`: the link to associate with the session
|
||||
/// * `id_token`: the ID token returned by the upstream OAuth provider, if
|
||||
/// present
|
||||
/// * `id_token_claims`: the claims contained in the ID token, if present
|
||||
/// * `extra_callback_parameters`: the extra query parameters returned in
|
||||
/// the callback, if any
|
||||
/// * `userinfo`: the user info returned by the upstream OAuth provider, if
|
||||
/// requested
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
#[expect(clippy::too_many_arguments)]
|
||||
async fn complete_with_link(
|
||||
&mut self,
|
||||
clock: &dyn Clock,
|
||||
upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession,
|
||||
upstream_oauth_link: &UpstreamOAuthLink,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
|
||||
@@ -131,6 +136,7 @@ repository_impl!(UpstreamOAuthSessionRepository:
|
||||
upstream_oauth_authorization_session: UpstreamOAuthAuthorizationSession,
|
||||
upstream_oauth_link: &UpstreamOAuthLink,
|
||||
id_token: Option<String>,
|
||||
id_token_claims: Option<serde_json::Value>,
|
||||
extra_callback_parameters: Option<serde_json::Value>,
|
||||
userinfo: Option<serde_json::Value>,
|
||||
) -> Result<UpstreamOAuthAuthorizationSession, Self::Error>;
|
||||
|
||||
Reference in New Issue
Block a user