feat(oidc): impl back channel logout
This commit is contained in:
@@ -253,6 +253,24 @@ pub struct OidcIdClaims {
|
||||
/// `LocaleRegistry`; ignored on subsequent logins so a later
|
||||
/// UI-driven choice isn't overwritten by the IdP.
|
||||
pub locale: Option<String>,
|
||||
/// OIDC session identifier. Populated only when the IdP emits `sid`
|
||||
/// on the id_token (Keycloak: "Backchannel Logout Session Required"
|
||||
/// on the client). When present, we persist it on the OxiCloud
|
||||
/// session so Back-Channel Logout can revoke that specific device.
|
||||
pub sid: Option<String>,
|
||||
}
|
||||
|
||||
/// OIDC Back-Channel Logout 1.0 identifiers extracted from a validated
|
||||
/// logout_token. The BCL handler uses these to resolve which OxiCloud
|
||||
/// session(s) to revoke: `sid` for per-device (preferred), else `sub` for
|
||||
/// all of the user's sessions.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OidcLogoutClaims {
|
||||
pub sub: Option<String>,
|
||||
pub sid: Option<String>,
|
||||
/// JWT identifier — used by the app service to prevent replay of the
|
||||
/// same logout_token within the token's freshness window.
|
||||
pub jti: Option<String>,
|
||||
}
|
||||
|
||||
/// Port for OIDC operations — implemented in infrastructure layer
|
||||
@@ -288,6 +306,20 @@ pub trait OidcServicePort: Send + Sync + 'static {
|
||||
/// Get the OIDC provider display name
|
||||
fn provider_name(&self) -> &str;
|
||||
|
||||
/// Validate an OIDC Back-Channel Logout 1.0 logout_token.
|
||||
///
|
||||
/// Enforces all mandatory spec checks: JWKS signature, iss+aud match,
|
||||
/// `events` claim contains the backchannel-logout URI, presence of
|
||||
/// `sub` and/or `sid`, absence of `nonce`. On any failure returns
|
||||
/// `AccessDenied` — the handler translates to a 400 per spec.
|
||||
///
|
||||
/// The caller is responsible for jti replay prevention (this validator
|
||||
/// is stateless).
|
||||
async fn validate_logout_token(
|
||||
&self,
|
||||
logout_token: &str,
|
||||
) -> Result<OidcLogoutClaims, DomainError>;
|
||||
|
||||
/// Build an RP-initiated logout URL (OIDC Session Management 1.0).
|
||||
///
|
||||
/// Returns `Ok(None)` when the IdP's discovery document does not advertise
|
||||
@@ -333,6 +365,21 @@ pub trait SessionStoragePort: Send + Sync + 'static {
|
||||
|
||||
/// Revokes all sessions in a token family (used when replay of a revoked token is detected)
|
||||
async fn revoke_session_family(&self, family_id: Uuid) -> Result<u64, DomainError>;
|
||||
|
||||
/// OIDC Back-Channel Logout: revoke sessions matching an IdP-supplied
|
||||
/// `sid` (per-device). Returns the user id(s) of revoked sessions so
|
||||
/// the caller can dispatch lifecycle hooks.
|
||||
async fn revoke_sessions_by_oidc_sid(&self, sid: &str) -> Result<Vec<Uuid>, DomainError>;
|
||||
|
||||
/// OIDC Back-Channel Logout fallback when the IdP didn't supply a `sid`:
|
||||
/// revoke every session belonging to the user identified by
|
||||
/// `(oidc_provider, oidc_subject)`. Returns the affected user id, or
|
||||
/// `None` if we don't know that user.
|
||||
async fn revoke_user_sessions_by_oidc_subject(
|
||||
&self,
|
||||
oidc_provider: &str,
|
||||
oidc_subject: &str,
|
||||
) -> Result<Option<Uuid>, DomainError>;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -123,6 +123,11 @@ pub enum LogoutReason {
|
||||
/// Refresh-token reuse detected by the session-family guard. Entire
|
||||
/// family revoked because the rotation was probably stolen.
|
||||
TokenReused,
|
||||
/// OIDC Back-Channel Logout — the IdP notified us that a session
|
||||
/// ended on its side (user logged out on another RP, or admin
|
||||
/// revoked the SSO session). Session(s) revoked without any user
|
||||
/// action on OxiCloud itself.
|
||||
IdpNotification,
|
||||
}
|
||||
|
||||
/// How aggressively `on_user_deleted` cleanup should run. Today both
|
||||
|
||||
Reference in New Issue
Block a user