feat(session): ensure dpop even with OIDC
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user