feat(oidc): RP initiator logout
request token invalidation to IdP (OIDC) on logout
This commit is contained in:
@@ -52,9 +52,10 @@ impl SessionRepository for SessionPgRepository {
|
||||
r#"
|
||||
INSERT INTO auth.sessions (
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked, family_id
|
||||
ip_address, user_agent, created_at, revoked, family_id,
|
||||
oidc_id_token
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||
)
|
||||
"#,
|
||||
)
|
||||
@@ -67,6 +68,7 @@ impl SessionRepository for SessionPgRepository {
|
||||
.bind(session_clone.created_at())
|
||||
.bind(session_clone.is_revoked())
|
||||
.bind(session_clone.family_id())
|
||||
.bind(session_clone.oidc_id_token())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
@@ -111,7 +113,8 @@ impl SessionRepository for SessionPgRepository {
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked, family_id
|
||||
ip_address, user_agent, created_at, revoked, family_id,
|
||||
oidc_id_token
|
||||
FROM auth.sessions
|
||||
WHERE id = $1
|
||||
"#,
|
||||
@@ -131,6 +134,7 @@ impl SessionRepository for SessionPgRepository {
|
||||
row.get("created_at"),
|
||||
row.get("revoked"),
|
||||
row.get("family_id"),
|
||||
row.get("oidc_id_token"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -144,7 +148,8 @@ impl SessionRepository for SessionPgRepository {
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked, family_id
|
||||
ip_address, user_agent, created_at, revoked, family_id,
|
||||
oidc_id_token
|
||||
FROM auth.sessions
|
||||
WHERE refresh_token = $1
|
||||
"#,
|
||||
@@ -164,6 +169,7 @@ impl SessionRepository for SessionPgRepository {
|
||||
row.get("created_at"),
|
||||
row.get("revoked"),
|
||||
row.get("family_id"),
|
||||
row.get("oidc_id_token"),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -176,7 +182,8 @@ impl SessionRepository for SessionPgRepository {
|
||||
r#"
|
||||
SELECT
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked, family_id
|
||||
ip_address, user_agent, created_at, revoked, family_id,
|
||||
oidc_id_token
|
||||
FROM auth.sessions
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
@@ -200,6 +207,7 @@ impl SessionRepository for SessionPgRepository {
|
||||
row.get("created_at"),
|
||||
row.get("revoked"),
|
||||
row.get("family_id"),
|
||||
row.get("oidc_id_token"),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -348,9 +356,10 @@ impl SessionStoragePort for SessionPgRepository {
|
||||
r#"
|
||||
INSERT INTO auth.sessions (
|
||||
id, user_id, refresh_token, expires_at,
|
||||
ip_address, user_agent, created_at, revoked, family_id
|
||||
ip_address, user_agent, created_at, revoked, family_id,
|
||||
oidc_id_token
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||
)
|
||||
"#,
|
||||
)
|
||||
@@ -363,6 +372,7 @@ impl SessionStoragePort for SessionPgRepository {
|
||||
.bind(session_clone.created_at())
|
||||
.bind(session_clone.is_revoked())
|
||||
.bind(session_clone.family_id())
|
||||
.bind(session_clone.oidc_id_token())
|
||||
.execute(&mut **tx)
|
||||
.await
|
||||
.map_err(Self::map_sqlx_error)?;
|
||||
|
||||
@@ -28,6 +28,10 @@ struct OidcDiscovery {
|
||||
token_endpoint: String,
|
||||
userinfo_endpoint: Option<String>,
|
||||
jwks_uri: String,
|
||||
/// RP-initiated logout endpoint (OIDC Session Management 1.0).
|
||||
/// Optional — not every IdP advertises it. When missing, callers
|
||||
/// must fall back to local-only logout.
|
||||
end_session_endpoint: Option<String>,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -533,6 +537,28 @@ impl OidcServicePort for OidcService {
|
||||
fn provider_name(&self) -> &str {
|
||||
&self.config.provider_name
|
||||
}
|
||||
|
||||
async fn build_end_session_url(
|
||||
&self,
|
||||
id_token_hint: &str,
|
||||
post_logout_redirect_uri: &str,
|
||||
) -> Result<Option<String>, DomainError> {
|
||||
let discovery = self.get_discovery().await?;
|
||||
let Some(endpoint) = discovery.end_session_endpoint else {
|
||||
return Ok(None);
|
||||
};
|
||||
// client_id is also included: some IdPs (Keycloak in "legacy" mode)
|
||||
// use it to look up the registered post_logout_redirect_uri when
|
||||
// the id_token_hint is expired or missing.
|
||||
let url = format!(
|
||||
"{}?id_token_hint={}&post_logout_redirect_uri={}&client_id={}",
|
||||
endpoint,
|
||||
urlencoding::encode(id_token_hint),
|
||||
urlencoding::encode(post_logout_redirect_uri),
|
||||
urlencoding::encode(&self.config.client_id),
|
||||
);
|
||||
Ok(Some(url))
|
||||
}
|
||||
}
|
||||
|
||||
// We need urlencoding — let's use a minimal inline implementation
|
||||
|
||||
Reference in New Issue
Block a user