Skip to main content

Session logout

Session logout ends one sign-in identified by sid. It is opt-in:

{
"SessionLogout": {
"Enabled": true
}
}

The default is false. Enable the setting consistently on every IDP instance only after completing the activation checks below. Restart instances when changing it. This setting does not enable front-channel notifications and does not change token lifetimes.

Behavior

A successful fresh sign-in creates a random sid in the shared master database and the Identity cookie. SSO into another application, cookie renewal, security-stamp principal renewal, authorization-code exchange and refresh retain that sid. Another browser sign-in gets an independent session, even for the same user.

User grants are bound to the session and application in separate OpenIddict ad hoc authorizations. Permanent authorizations remain consent records. Logout ends the session and revokes its grants across applications without revoking another session or permanent consent.

Session identity uses the standard sid claim. During code/device-code exchange and refresh, the outgoing identity receives sid from the credential authenticated by OpenIddict. User authorizations store ava_session_id for grant binding and revocation. Client-credentials issuance bypasses session handling and keeps the existing OpenIddict behavior, without a separate service authorization or grant-kind marker. A user credential without sid is rejected.

Existing signed access JWTs remain valid at resource APIs until their configured expiration plus the API's clock skew. IDP denies further token issuance, refresh and silent SSO for an ended session. Apps may therefore still display a signed-in state until their next authentication check. Instant client UI logout and front-channel delivery belong to AVP-925.

The existing federation/impersonation token endpoint keeps its JWT type, audience and 600-second lifetime. With session enforcement enabled, user-derived JWTs include sid, client_id, oi_au_id (the source authorization ID) and oi_tkn_id (the new persisted token ID). These bindings let IDP validate a derived token as the source of further derivation and reject it after logout. The filter removes copied issuance metadata before setting these bindings; it does not remove all private claims from the final JWT. aud is set by the token descriptor to the issuer. Consumers should treat the record IDs as opaque. Each derivation retains the session and grant, creates its own token record and passes the same final issuance check.

OpenIddict validates token and authorization entries; the session handler adds only the active-session check for authorization codes, refresh tokens and approved device credentials. Access-token validation keeps its existing behavior. The user-derived token endpoint separately validates the source token, authorization and session because it runs outside the OpenIddict server pipeline. Password sign-in binds to its new cookie session; user approval binds pending device codes to the approving session; login_behalf_of creates a new user session.

Device approval builds the client, tenant and organization claims regardless of SessionLogout:Enabled. If the selected context is unavailable, it uses the same organization recovery as interactive authorization and returns tenant_organization_switched after a successful switch, just like interactive authorization. The client must then restart device authorization and request approval in the recovered context. This device-flow correction is part of the build, not the logout flag. Rolling back to an older build also removes this correction.

Front-channel participant lookup

FrontChannelLogoutService.GetTargetsAsync prepares registered callback URLs for a session that the caller has already validated and ended. The caller supplies the trusted IDP issuer. It uses the existing indexed ava_session_id authorization lookup, including revoked grants, and returns each participating application once. Missing or deleted applications and clients without a callback are skipped.

Callbacks are read and revalidated with the same helper used by the Clients API. Ordinary query parameters are preserved; iss and sid are appended with URL encoding. Each target contains the client ID, prepared URL and browser origin for later acknowledgement checks. Invalid callbacks and storage errors propagate instead of returning an empty or partial success. They cannot undo the already completed session termination.

The resolver is registered in DI but is not called by the logout flow yet. It performs no HTTP requests and does not enable browser delivery or advertise front-channel support in discovery.

Logout requests

GET and POST /connect/endsession share protocol and session validation:

  • A valid ID token hint with a matching browser cookie ends the session directly.
  • A missing hint, or a hint without a browser cookie, requires confirmation. The form carries a protected ten-minute request envelope and an antiforgery token. Changed parameters and replay against a different session are rejected.
  • OpenIddict validates the ID token hint, client and registered redirect URI before both GET and POST reach the controller. Session handling then checks the subject and session against the browser cookie. Session identity comes from the hint's sid, or from the cookie when no hint is supplied. The optional logout_hint is opaque: it does not select a session and need not equal sid. Invalid ID token hints never fall back to cookie-only success.
  • An expired ID token can be a logout hint, including after token-record cleanup. This exception applies only to an ID token at the end-session endpoint. It does not relax signature, client, session or resource-API validation.
  • Unknown sessions and mismatches produce a local error. Persistence failure before the durable session end produces a retryable failure without a success redirect. Once the end is durable, physical grant revocation may finish asynchronously.

The confirmation and error pages use the existing login layout and Czech, Slovak and English translations. No refresh token, access token or credentials are included in the confirmation form.

Persistence and concurrent requests

IDP.AuthenticationSessions is stored alongside OpenIddict's master collections. Single-session decisions read the primary with linearizable concern on replica sets, or the sole authoritative node on a standalone. Session end and retention extension use majority-acknowledged, journaled writes. Reads and write acknowledgements have a ten-second server wait limit. A quorum failure never falls back to weaker replica-set reads. See MongoDB's consistency guarantees.

Before persisting each user token, a conditional write extends retention only while the session is active. After generating all tokens, IDP reads the session again using the same authoritative read as other session checks. If the session has ended, the complete response is rejected and its generated records are revoked. No extra counter or session write is needed for this decision. A logout that finishes before this final read starts is observed. Overlapping issuance and logout can still result in a response reaching the client after logout; already issued access JWTs follow the existing expiration contract.

The retention deadline grows to cover every cookie renewal and token expiration, including long offline refresh lifetimes. Non-expiring tokens retain the session indefinitely. Retention is cleanup metadata, not a new idle or absolute authentication timeout. Active sessions with no remaining retained credential are ended after an additional hour of grace.

The session document records pending physical revocation before cleanup starts. A Quartz job retries in batches of at most 100 sessions with per-session failure backoff. Successful sweeps repeat hourly while credentials could still be outstanding, covering a process crash or late token write. Once finite retention has elapsed and revocation succeeds, the TTL index removes the document after a further hour. Unknown sessions are always denied. Multiple workers can safely repeat revocation.

OIDCOptions:UseQuartzSheduler=false disables OpenIddict pruning: token and authorization records are not automatically deleted by that scheduler. The session revocation job still runs when session logout is enabled, but changes grant status and cleans up session records; it does not replace OpenIddict pruning.

AuthenticationSessionIndexMigration_924 creates the session cleanup and TTL indexes and an authorization index on properties.ava_session_id. No legacy session backfill runs.

Indefinite retention follows the existing contract for credentials without an expiration. Such sessions keep the durable revocation record and the hourly cleanup sweep, even after successful revocation. Before activation, check whether clients actually issue non-expiring credentials and monitor pending-revocation age and volume. Changing this policy requires a separate decision about cleanup of late token writes; it is not a test-infrastructure optimization.

Test selection

The protocol and refresh-compatibility classes each share an enabled application host. Each test owns its browser cookies, user and client identifiers; tests with different authentication settings, feature activation or a controlled logout race keep separate hosts. Persistence tests reuse the existing MongoDB platform directly, without starting another application. They read stored authorization/token status and cover retry backoff, cleanup eligibility and index migration repeatability. The Quartz check waits for completion of the actual revocation job with a deadline.

For a normal local iteration, run from backend:

dotnet test 'ASOL.IdentityProvider.IntegrationTests/ASOL.IdentityProvider.IntegrationTests.csproj' `
--filter 'Category!=ReplicaSet'

Run the replica-set scenario separately after changes to session persistence, logout/token guards or the MongoDB driver, and before enabling the feature:

dotnet test 'ASOL.IdentityProvider.IntegrationTests/ASOL.IdentityProvider.IntegrationTests.csproj' `
--filter 'Category=ReplicaSet'

The current Plaza/Deployment release templates do not expose a test-filter parameter and skip integration tests outside PRs targeting develop or release. The existing PR check therefore continues running the complete integration project, including the replica-set scenario. Exclude it from that check only after a separate CI job with the required environment and triggers is operational. An unsupported container runtime remains an explicit failure, not a silent skip. The process-failure scenario proves denial and recovery, not network-partition linearizability or a particular HTTP error mapping; deterministic middleware tests separately verify the 503 response.

The fast service/handler/cookie tests use mocks and remain in the integration project, which references the API. Moving them to another project must also enable that project in CI; IdentityProvider-Check.yml currently has RunUnitTests: false. A local run excluding the replica set is not the complete pre-commit validation: run dotnet test 'ASOL.IdentityProvider.sln' before committing backend changes.

Activation and rollback

Run the focused suite from backend with:

dotnet test 'ASOL.IdentityProvider.IntegrationTests/ASOL.IdentityProvider.IntegrationTests.csproj' `
--filter 'FullyQualifiedName~SessionLogout'

The protocol tests use the existing shared Testcontainers platform, real Identity cookies, PKCE exchange, refresh, logout and the impersonation endpoint. Most use a standalone MongoDB. SessionReplicaSetTests additionally copies the IDP collections from the disposable test master database into a three-member replica set and runs the real login, code exchange, logout and refresh endpoints against it. It kills the primary process, checks election and durable logout, kills both secondaries to remove the majority, and checks that session reads and refresh fail without returning tokens. After recovery, an independent active session must work again while the ended session remains rejected.

Run only this fault scenario from backend with:

dotnet test 'ASOL.IdentityProvider.IntegrationTests/ASOL.IdentityProvider.IntegrationTests.csproj' `
--filter 'FullyQualifiedName~SessionReplicaSetTests'

The fixture uses the configured MongoDB test image and a local Docker-compatible runtime. Three independent mongod processes use separate data directories and loopback ports in one disposable container; fault injection never stops the container runtime or other workloads. This covers member crashes, election, majority loss and recovery locally. It does not simulate separate-machine network partitions or validate the deployed environment's TLS, credentials and client UI recovery. Those still require environment acceptance. Run dotnet test 'ASOL.IdentityProvider.sln' for the complete backend check.

  1. Deploy the compatible build with the feature disabled. Verify device approval, exchange and organization recovery with the feature disabled. Verify migration completion, primary/majority MongoDB connectivity, shared data-protection keys and signing keys across IDP instances. Verify the session revocation job starts when enabled, even if the optional OpenIddict pruning scheduler is disabled.
  2. Validate the enabled build in a test environment with existing clients: a legacy user refresh fails with invalid_grant, silent SSO fails with login_required, and the client returns to interactive login without a refresh loop or blank page. One fresh sign-in is expected. Legacy cookies and user tokens are not migrated. Service credentials must continue working.
  3. Verify two applications sharing one sid, an independent browser session, confirmation with and without a hint, persistence failure, refresh racing logout, and user-derived token issuance. Confirm that resource APIs still accept already issued JWTs until their existing expiration plus skew.
  4. Activate all IDP instances together. A mixed fleet with old issuers or disabled guards is unsupported. Monitor authentication failures and the age of pending revocations. Client behavior must be accepted before production activation.

Before activation, the disabled deployment can be rolled back normally. After sessions have been issued and ended, rollback must use a compatible build that continues to enforce their ended state. Disabling the guards or redeploying a pre-session build can accept old cookies or grants and is not a safe rollback. Preserve session records and signing/data-protection keys during recovery. Do not delete ended sessions to resolve an availability incident.

Related work: AVP-924, AVP-925.