feat(session): ensure dpop even with OIDC

This commit is contained in:
Edouard Vanbelle
2026-08-09 05:00:30 +02:00
parent bee856fbd0
commit 10d831b204
7 changed files with 103 additions and 14 deletions
+23
View File
@@ -50,16 +50,38 @@ pub struct SessionSummaryDto {
/// IdP emitted `sid` — otherwise `None`. Useful when an operator is
/// correlating with the upstream IdP's session log.
pub oidc_sid: Option<String>,
/// `true` when this row IS the caller's currently-active session —
/// set by the service layer by comparing the row's `dpop_jkt` with
/// the caller's own bound thumbprint. Lets the admin panel flag
/// "revoking this cuts your own branch" so an admin doesn't
/// accidentally log themselves out. `false` when either side is
/// unbound (can't correlate) or when the jkts don't match.
pub is_current: bool,
}
impl From<Session> for SessionSummaryDto {
fn from(s: Session) -> Self {
Self::from_session(s, None)
}
}
impl SessionSummaryDto {
/// Build the DTO with an optional `caller_jkt` used to compute
/// `is_current`. Pass the admin caller's DPoP thumbprint to have
/// the panel highlight the caller's own row; pass `None` when
/// the caller is unbound (no jkt = no correlation) or from
/// non-admin contexts.
pub fn from_session(s: Session, caller_jkt: Option<&str>) -> Self {
let is_revoked = s.is_revoked();
let is_expired = s.is_expired();
let jkt = s.dpop_jkt().map(|s| s.to_owned());
let dpop_jkt_prefix = jkt
.as_ref()
.map(|t| t.chars().take(8).collect::<String>());
let is_current = match (jkt.as_deref(), caller_jkt) {
(Some(row), Some(caller)) => row == caller,
_ => false,
};
Self {
id: s.id(),
user_id: s.user_id(),
@@ -72,6 +94,7 @@ impl From<Session> for SessionSummaryDto {
is_revoked,
is_active: !is_revoked && !is_expired,
oidc_sid: s.oidc_sid().map(str::to_owned),
is_current,
}
}
}
@@ -2963,6 +2963,7 @@ impl AuthApplicationService {
&self,
authorization: &A,
caller_id: Uuid,
caller_dpop_jkt: Option<&str>,
user_id_filter: Option<Uuid>,
include_revoked: bool,
limit: i64,
@@ -2975,7 +2976,12 @@ impl AuthApplicationService {
.await?;
Ok(sessions
.into_iter()
.map(crate::application::dtos::session_dto::SessionSummaryDto::from)
.map(|s| {
crate::application::dtos::session_dto::SessionSummaryDto::from_session(
s,
caller_dpop_jkt,
)
})
.collect())
}
@@ -1238,11 +1238,20 @@ pub async fn list_sessions(
None => None,
};
// Pass the caller's DPoP thumbprint so the DTO can flag which
// row is the admin's own current session (`is_current = true`).
// Rendered as a "this is you" badge — prevents the admin from
// accidentally revoking the session they're clicking from.
// `None` when the admin is unbound (rare — legacy / migration
// window sessions), in which case no row highlights.
let caller_jkt = auth_user.dpop_jkt.as_deref();
let sessions = auth
.auth_application_service
.admin_list_sessions_with_perms(
state.authorization.as_ref(),
auth_user.id,
caller_jkt,
user_id_filter,
include_revoked,
limit,