Skip to main content

Session logout

AVP-924 ends the IDP sign-in and prevents further token issuance for that session. AVP-925 adds browser notifications so participating applications can clear their local sign-in state. Frontend teams can follow the integration guide below; IDP operators must also follow activation and rollback.

Session logout ends one sign-in identified by sid. Session enforcement and front-channel delivery are independently opt-in; their configuration is described in activation and rollback.

Frontend integration guide​

1. Connect application logout and login recovery​

Use your OIDC library's logout operation to navigate to the IDP end_session_endpoint from discovery. Pass the session's ID token as id_token_hint and a registered post_logout_redirect_uri. Clearing the application's local state alone does not end the IDP session. See logout requests for confirmation behavior.

Handle a rejected refresh (invalid_grant) or silent sign-in (login_required) by clearing the application's stale authentication state and offering interactive sign-in. Stop automatic retries for the ended session. This recovery is required even when no front-channel callback is registered or the browser cannot deliver it. Enabling AVP-924 also requires one fresh sign-in for users with legacy credentials.

2. Implement the application's callback​

Expose a lightweight route, for example https://app.example.test/auth/front-channel-logout, following the callback contract. Reuse a compatible front-channel handler from your authentication library if available.

IDP loads the route in a hidden iframe, for example:

https://app.example.test/auth/front-channel-logout?iss=https%3A%2F%2Fidp.example.test%2Fapi%2Fasol%2Fidp&sid=example-session-id

Keep the session's iss and sid from the ID token validated by your OIDC library. Compare the callback parameters with that trusted session and the configured issuer. The following browser-client pseudocode shows the integration point; auth represents your application's existing authentication layer, not a new IDP or SDK API:

const expectedIssuer = 'https://idp.example.test/api/asol/idp';
const query = new URLSearchParams(window.location.search);
const session = auth.readValidatedSession();

if (session?.sid &&
query.getAll('iss').length === 1 &&
query.getAll('sid').length === 1 &&
session.iss === expectedIssuer &&
query.get('iss') === expectedIssuer &&
query.get('sid') === session.sid) {
auth.clearMatchingLocalSession(expectedIssuer, session.sid);
}

Implement the two adapter methods with the application's actual storage and authentication library. Clearing must remove the matching tokens and cached user state and update already-open tabs, using the application's existing tab coordination or a session-scoped storage/BroadcastChannel event. A callback iframe cannot directly clear another tab's in-memory state or sessionStorage.

For a server-managed session or HttpOnly cookie, perform the matching-session logout in the server callback before returning the document. Browser JavaScript cannot clear an HttpOnly cookie.

Allow the callback route to be embedded by the configured IDP origin. For this example, its response headers include:

Cache-Control: no-store
Content-Security-Policy: frame-ancestors https://idp.example.test

Keep the application's other CSP directives and restrict this embedding exception to the callback route. Ensure proxy headers do not block it with X-Frame-Options: DENY or, for different origins, SAMEORIGIN. For browser cleanup, use a synchronous defer script to meet the callback completion rules.

3. Register the callback in IDP​

The client administrator registers the URL through the Clients API, using an authorized account in the owning tenant. This is an administration operation, not a request made by the frontend at every login.

First read GET /api/asol/idp/api/v1/Clients/example-frontend. Preserve the current display name, grants, integration token and complete redirect/post-logout lists when preparing the PUT body. The example below assumes the existing client has exactly these values; replace them with the preceding GET result for your client:

PUT https://idp.example.test/api/asol/idp/api/v1/Clients/example-frontend
Authorization: Bearer <client-administration-access-token>
Content-Type: application/json

{
"clientName": "Example frontend",
"grants": "AuthorizationCode",
"redirectUris": ["https://app.example.test/signin-oidc"],
"postLogoutUris": ["https://app.example.test/"],
"frontChannelLogoutUri": "https://app.example.test/auth/front-channel-logout",
"integrationToken": null
}

The callback must use HTTPS and match the scheme, host and effective port of a registered redirectUris entry. Register the base callback URL without iss or sid; IDP appends them. postLogoutUris contains user return destinations, while frontChannelLogoutUri receives the hidden notification. GET the client again to verify the saved URL. On update, an omitted or null callback preserves it; an empty string removes it. See registration rules for full validation details.

4. Enable and verify delivery​

Coordinate activation with the IDP operator using delivery rollout and rollback. See front-channel notifications for timing and delivery limits.

Verify with two applications and fresh sign-ins:

  • Both applications share the same sid; logout in either clears the other application's matching state, including its open tabs.
  • A separate browser sign-in has a different sid and remains active.
  • A missing or stalled callback does not block logout: IDP continues automatically. A client that missed the notification handles rejected refresh and returns to interactive sign-in.
  • Repeating a callback is harmless; an issuer or session mismatch leaves the active session unchanged. Check iframe embedding and storage access in the browsers and deployment origins you support.

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 still display a signed-in state until their next authentication check. The optional front-channel notification below asks them to clear their local state but does not confirm that they have done so.

The global default for new OIDC access tokens is ten minutes, configured independently through OIDCOptions:AccessTokenLifetimeSeconds. Client-specific OpenIddict settings can override it. See access-token lifetime for configuration and existing-token behavior.

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 the page's frame-src policy. Invalid registrations are logged by client ID and skipped, so they do not block valid recipients. Storage errors propagate to the notification handler, which logs the failure and lets the normal logout redirect continue. Neither failure can undo the already completed session termination.

Front-channel notifications​

When delivery is enabled, discovery advertises frontchannel_logout_supported and frontchannel_logout_session_supported as true; otherwise these metadata values are omitted. Configuration and prerequisites are in activation and rollback.

After a validated logout request has durably ended the session and cleared the IDP cookie, the end-session response handler prepares the recipients and renders their callbacks in parallel iframes. The issuer comes from the configured OpenIddict issuer, exactly as in discovery and issued tokens. The continuation URL comes from the validated OpenIddict response, including state; it defaults to / when no post-logout redirect is requested. No new logout or continuation endpoint is needed.

  • With no valid registered recipients, or if recipient lookup fails, the normal logout redirect runs immediately.
  • IDP automatically continues after every iframe fires its load event or after five seconds from the start of loading, whichever comes first. A failed or stalled callback does not require manual continuation.
  • The load event only bounds the wait. It is not evidence of successful delivery or local logout, and IDP does not evaluate per-application results or exchange acknowledgement messages.
  • The page says "You are signed out", referring to the ended IDP session. It does not claim that all applications have signed out and does not list unconfirmed applications.
  • Without JavaScript, automatic notification and continuation are unavailable; the page explains this and offers a continuation link.

The page uses the existing login layout with Czech, Slovak and English resources. Responses use Cache-Control: no-store, Referrer-Policy: no-referrer, frame-ancestor protection and a CSP whose frame-src permits only the valid recipients' origins. Scripts and stylesheets are same-origin; font-src also permits https://avaplace.com for the shared layout's Geomanist font.

Client callback contract​

The iframe URL and its iss and sid parameters follow OpenID Connect Front-Channel Logout. No custom postMessage handshake or acknowledgement is required.

  1. Loading the registered callback URL triggers logout. The callback verifies iss and sid against the client's trusted authentication context; the issuer in the query alone is not trusted configuration. The route remains accessible when already signed out and must not redirect to login or start another IDP logout.
  2. The client clears only the matching local authentication state, preserving independent sessions and unrelated storage. A missing or mismatched issuer or session leaves the state unchanged. Repeated requests and an already-cleared session must be safe. The callback must not wait for a message from IDP before performing logout.
  3. The callback allows embedding by the IDP origin, returns Cache-Control: no-store, and completes its logout work before the callback document's load event, without top-level navigation. Do not defer essential cleanup to a timer: IDP may navigate away as soon as the iframe loads.

Third-party cookie/storage restrictions can prevent local logout in an iframe. Delivery is best effort and does not guarantee that every application cleared its state. Access-token lifetime and enforcement are described in session behavior.

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​

These are two independent IDP settings, both defaulting to false. The configuration with both features enabled is:

{
"SessionLogout": { "Enabled": true },
"FrontChannelLogout": { "Enabled": true }
}

Enabling delivery without session enforcement fails startup. Change each setting consistently on every IDP instance and restart the instances. Session enforcement can remain enabled while delivery is disabled.

Session enforcement​

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.

Delivery rollout and rollback​

Deploy IDP with FrontChannelLogout:Enabled=false first and complete the session enforcement activation before enabling delivery. Deploy and verify each client's callback implementation before registering its frontChannelLogoutUri. After validating protocol and browser behavior with those clients in a test environment, enable FrontChannelLogout:Enabled=true using the instance configuration procedure above. Other clients can be added later without changing the session model.

To stop notifications, set FrontChannelLogout:Enabled=false and restart the instances. Keep SessionLogout:Enabled=true and retain the compatible session implementation, records and keys. The session rollback restrictions above still apply. Disabling notifications does not undo completed session termination. Client SDK/application changes, token lifetime changes and local PoC launchers are outside this delivery change.

Related work: AVP-924, AVP-925.