@@ -5,20 +5,6 @@ import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger'
55import { getOryIdentityApi } from './client'
66import { readOryError } from './ory-error'
77
8- /**
9- * Revokes every Kratos identity session for the given identity.
10- *
11- * Hydra's /oauth2/sessions/logout only ends the OAuth2 session; the Kratos
12- * identity cookie on the Ory domain is independent and is what causes the
13- * Account Experience to show "Reauthenticate as <last user>" on the next
14- * sign-in instead of a fresh provider chooser.
15- *
16- * We can't surgically target a single session because the OIDC `sid` claim
17- * from Hydra is Hydra's own OAuth2 session id, not a Kratos session id, and
18- * we don't have access to the user's Kratos cookie from this side. Revoking
19- * all identity sessions matches the expected "sign out of identity provider"
20- * semantics anyway.
21- */
228// Ory uses optimistic locking on identity rows; concurrent writes (e.g. our
239// admin DELETE racing with Hydra's RP-initiated logout cleanup during the
2410// same signout flow) return 429 with reason "Conflicting concurrent
@@ -27,12 +13,48 @@ import { readOryError } from './ory-error'
2713const REVOKE_MAX_ATTEMPTS = 3
2814const REVOKE_BACKOFF_MS = 150
2915
16+ /**
17+ * Revokes a single Kratos session by its session id (admin DELETE
18+ * /admin/sessions/{id}).
19+ *
20+ * This is the server-side equivalent of the browser self-service logout: it
21+ * ends only the current session, preserving single sign-out semantics. We call
22+ * it on sign-out because Hydra's /oauth2/sessions/logout skips the dashboard
23+ * /logout -> Kratos bridge whenever Hydra holds no active authentication
24+ * session (the production default, where the login is accepted with
25+ * remember=false), which would otherwise leave the Kratos identity session
26+ * alive and surface "Reauthenticate as <last user>" on the next sign-in.
27+ */
28+ export async function revokeKratosSession ( sessionId : string ) : Promise < void > {
29+ await revokeWithRetries ( 'revoke_kratos_session' , ( ) =>
30+ getOryIdentityApi ( ) . disableSession ( { id : sessionId } )
31+ )
32+ }
33+
34+ /**
35+ * Revokes every Kratos identity session for the given identity (admin DELETE
36+ * /admin/identities/{id}/sessions).
37+ *
38+ * Used after a credential change, where signing out every device is the
39+ * intended "sign out of identity provider" behavior. The OIDC `sid` claim from
40+ * Hydra is Hydra's own OAuth2 session id, not a Kratos session id, so
41+ * single-session targeting isn't available on that path.
42+ */
3043export async function revokeKratosSessionsForIdentity (
3144 identityId : string
45+ ) : Promise < void > {
46+ await revokeWithRetries ( 'revoke_kratos_sessions' , ( ) =>
47+ getOryIdentityApi ( ) . deleteIdentitySessions ( { id : identityId } )
48+ )
49+ }
50+
51+ async function revokeWithRetries (
52+ operation : string ,
53+ run : ( ) => Promise < unknown >
3254) : Promise < void > {
3355 for ( let attempt = 1 ; attempt <= REVOKE_MAX_ATTEMPTS ; attempt ++ ) {
3456 try {
35- await getOryIdentityApi ( ) . deleteIdentitySessions ( { id : identityId } )
57+ await run ( )
3658 return
3759 } catch ( error ) {
3860 if ( error instanceof ResponseError && error . response . status === 404 ) {
@@ -53,11 +75,11 @@ export async function revokeKratosSessionsForIdentity(
5375
5476 l . error (
5577 {
56- key : ' auth_provider:revoke_kratos_sessions :error' ,
78+ key : ` auth_provider:${ operation } :error` ,
5779 context : { ory : oryDetails , attempt } ,
5880 error : serializeErrorForLog ( error ) ,
5981 } ,
60- 'failed to revoke Kratos sessions ; user may see reauth UX on next sign-in'
82+ 'failed to revoke Kratos session(s) ; user may see reauth UX on next sign-in'
6183 )
6284 return
6385 }
0 commit comments