← Duende SkillsCONTENT HISTORYWHAT CHANGED · RULE-BASED ANALYSIS
Update to Duende Skills
Snapshot Sep 30, 2026 · 23:14 UTC · version 0.3.0
Collection source: not recorded for this historical snapshot.
First saved snapshot
No earlier snapshot is available to establish a change.
Compare saved observations
Download comparison JSONFull technical diff · 0 changed fields
Full snapshot data
{
"name": "identityserver-sessions-providers",
"description": "Guide for configuring server-side sessions, session management and querying, inactivity timeout, dynamic identity providers, and CIBA (Client Initiated Backchannel Authentication) in Duende IdentityServer.",
"included_files": [],
"skill_md_contents": "---\nname: identityserver-sessions-providers\ndescription: \"Guide for configuring server-side sessions, session management and querying, inactivity timeout, dynamic identity providers, and CIBA (Client Initiated Backchannel Authentication) in Duende IdentityServer.\"\ninvocable: false\n---\n\n# IdentityServer Sessions, Dynamic Providers, and CIBA\n\n## When to Use This Skill\n\n- Enabling and configuring server-side sessions for authentication state management\n- Implementing session querying, revocation, and administrative tooling via `ISessionManagementService`\n- Configuring inactivity timeout across IdentityServer and client applications\n- Setting up the Entity Framework Core session store or implementing a custom `IServerSideSessionStore`\n- Adding dynamic identity providers loaded from a database at runtime\n- Implementing custom non-OIDC dynamic provider types (Google, SAML, etc.)\n- Building a CIBA (Client Initiated Backchannel Authentication) flow\n- Understanding edition requirements (Business vs Enterprise) for these features\n\nDocs: https://docs.duendesoftware.com/identityserver/ui/server-side-sessions/\n\n## Server-Side Sessions\n\n### What Problem Do They Solve?\n\nBy default, ASP.NET Core stores all authentication session state in a self-contained cookie. This creates several challenges:\n\n| Problem | Impact |\n| ---------------------------- | --------------------------------------------------------------------------------- |\n| Cookie size growth | As clients are tracked, the cookie grows; large cookies can exceed browser limits |\n| No session visibility | Cannot query how many active sessions exist |\n| No administrative revocation | Cannot terminate a session from outside the user's browser |\n| No server-side coordination | Cannot detect inactivity or synchronize session expiration across clients |\n\nServer-side sessions store authentication state on the server, keeping only a session reference in the cookie.\n\n### Edition Requirements\n\nServer-side sessions are part of the **Duende IdentityServer Business and Enterprise Edition**.\n\n### Enabling Server-Side Sessions\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n .AddServerSideSessions();\n```\n\n**Important**: This call must come after any custom `IRefreshTokenService` implementation registration. Order matters in the ASP.NET Core service provider.\n\nBy default, sessions are stored in-memory. For production, use Entity Framework Core or a custom store.\n\n### Using Entity Framework Core Store\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n .AddServerSideSessions()\n .AddOperationalStore(options =>\n {\n options.ConfigureDbContext = builder =>\n builder.UseSqlServer(connectionString,\n sql => sql.MigrationsAssembly(migrationsAssembly));\n });\n```\n\nThe EF Core implementation is included in the operational store and supports the `IServerSideSessionStore` interface automatically.\n\n### Custom Session Store\n\nImplement `IServerSideSessionStore` and register it:\n\n```csharp\n// Program.cs — two-step registration\nbuilder.Services.AddIdentityServer()\n .AddServerSideSessions()\n .AddServerSideSessionStore<YourCustomStore>();\n\n// Or one-step registration\nbuilder.Services.AddIdentityServer()\n .AddServerSideSessions<YourCustomStore>();\n```\n\n### Data Stored Server-Side\n\nThe session stores the serialized ASP.NET Core `AuthenticationTicket` (all claims + `AuthenticationProperties.Items`). The data is protected using ASP.NET Core's Data Protection API.\n\nQueryable indices extracted from the session:\n\n| Index | Source |\n| ------------ | ------------------------------------------------- |\n| Subject ID | `sub` claim value |\n| Session ID | `sid` claim value |\n| Display Name | Configurable claim type (e.g., `name` or `email`) |\n\nConfigure the display name claim. **Note**: `UserDisplayNameClaimType` is **unset (null) by default** due to PII concerns. You must explicitly set it if you want display names stored in the session index:\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer(options => {\n options.ServerSideSessions.UserDisplayNameClaimType = \"name\";\n}).AddServerSideSessions();\n```\n\n## Session Management with ISessionManagementService\n\n### Querying Sessions\n\n```csharp\nvar userSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery\n{\n CountRequested = 10,\n SubjectId = \"12345\",\n DisplayName = \"Bob\",\n});\n```\n\n### Paging Through Results\n\n```csharp\n// First page\nvar userSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery\n{\n CountRequested = 10,\n});\n\n// Next page\nuserSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery\n{\n ResultsToken = userSessions.ResultsToken,\n CountRequested = 10,\n});\n\n// Previous page\nuserSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery\n{\n ResultsToken = userSessions.ResultsToken,\n RequestPriorResults = true,\n CountRequested = 10,\n});\n```\n\n### Performance Note on Querying\n\nWhen listing sessions, prefer `GetSessionsAsync` over `QuerySessionsAsync`. The `QuerySessionsAsync` method performs a full-text search and may be slower. Use `QuerySessionsAsync` only when advanced filtering is needed.\n\n### Terminating Sessions\n\nTerminate sessions and optionally revoke tokens, consents, and send back-channel logout notifications:\n\n```csharp\n// Revoke everything for a user\nawait _sessionManagementService.RemoveSessionsAsync(new RemoveSessionsContext\n{\n SubjectId = \"12345\"\n});\n```\n\nSelective revocation (filtering by `SessionId` or `ClientIds` is also supported):\n\n```csharp\n// Only revoke refresh tokens, keep session and consents\nawait _sessionManagementService.RemoveSessionsAsync(new RemoveSessionsContext\n{\n SubjectId = \"12345\",\n SessionId = \"abc123\", // optional: target a specific session\n ClientIds = { \"my_app\" }, // optional: target specific clients\n RevokeTokens = true,\n RemoveServerSideSession = false,\n RevokeConsents = false,\n SendBackchannelLogoutNotification = false,\n});\n```\n\n### What Gets Cleaned Up\n\n| Flag | Effect |\n| --------------------------------------------------- | ---------------------------------------------------------------- |\n| `RemoveServerSideSession` (default: true) | Deletes the session record from the store |\n| `RevokeTokens` (default: true) | Revokes refresh tokens and reference access tokens |\n| `RevokeConsents` (default: true) | Removes persisted consent grants |\n| `SendBackchannelLogoutNotification` (default: true) | Sends back-channel logout to clients with `BackChannelLogoutUri` |\n\nInternally, this uses `IServerSideTicketStore`, `IPersistedGrantStore`, and `IBackChannelLogoutService`.\n\n## Server-Side Session Custom Metadata\n\nStore per-sign-in metadata (device name, auth method, region) in the `AuthenticationTicket`'s `AuthenticationProperties.Items`. It stays inside IdentityServer and is **NOT** issued as claims.\n\n### Writing Metadata\n\n```csharp\nvar properties = new AuthenticationProperties();\nproperties.Items[\"device_name\"] = \"Bob's iPhone\";\nproperties.Items[\"region\"] = \"eu-west\";\n\nawait HttpContext.SignInAsync(identityServerUser, properties);\n```\n\nWith ASP.NET Identity, pass `properties` to `SignInWithClaimsAsync`:\n\n```csharp\nawait _signInManager.SignInWithClaimsAsync(user, properties, additionalClaims: []);\n```\n\nIf you use `PasswordSignInAsync` (which does not accept properties), override `SignInWithClaimsAsync` in a custom `SignInManager` to inject the metadata.\n\n### Reading Metadata\n\n```csharp\nvar sessions = await _sessionManagementService.QuerySessionsAsync(\n new SessionQuery { SubjectId = \"12345\" });\n\nforeach (var session in sessions.Results)\n{\n if (session.AuthenticationTicket.Properties.Items\n .TryGetValue(\"device_name\", out var deviceName))\n {\n // use deviceName\n }\n}\n```\n\n**Limitation**: custom metadata is **not indexed** by the built-in store — you cannot filter `SessionQuery` by it. Query by subject id, session id, or display name first, then inspect the tickets.\n\n## Inactivity Timeout\n\n### The Challenge\n\nOpenID Connect does not natively provide distributed session management based on user inactivity. Multiple artifacts (cookies, refresh tokens, access tokens) have independent lifetimes controlled by different entities. Coordinating their expiration is non-trivial.\n\n### Design: Centralized Session Tracking\n\nServer-side sessions at IdentityServer provide the central record for monitoring user activity:\n\n1. **Activity signals**: As the user's client uses refresh tokens, introspection, or userinfo, these protocol calls extend the server-side session automatically via an internal `ISessionCoordinationService` (this is an implementation detail, not a public API for consumers).\n2. **Inactivity detection**: When no activity occurs within the session timeout, the session expires and cleanup is triggered (back-channel logout, token revocation).\n\n### Configuration at IdentityServer\n\nThree features must be enabled:\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer(options =>\n{\n // 1. Enable server-side sessions\n // (done separately via .AddServerSideSessions())\n\n // 2. Coordinate client token lifetimes with the user session\n options.Authentication.CoordinateClientLifetimesWithUserSession = true;\n\n // 3. Trigger back-channel logout when sessions expire\n // This is already true by default, shown here for explicitness\n options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;\n}).AddServerSideSessions();\n```\n\n**Note**: `ExpiredSessionsTriggerBackchannelLogout` defaults to `true`, so step 3 is technically optional. The only setting you must explicitly enable is `CoordinateClientLifetimesWithUserSession` (step 2).\n\nAlternatively, enable coordination per-client:\n\n```csharp\nvar client = new Client\n{\n ClientId = \"my_app\",\n CoordinateLifetimeWithUserSession = true\n};\n```\n\n### Client-Side Configuration\n\n| Client Type | How Activity Is Signaled | How Inactivity Is Detected |\n| ----------------------------------------- | ----------------------------------------- | -------------------------------------------------------------- |\n| Client with refresh tokens | Refresh token requests extend the session | Handle refresh token failure, or implement back-channel logout |\n| Client with reference tokens (no refresh) | Introspection extends the session | Handle `401` from API, or implement back-channel logout |\n| Client without access tokens | Cannot signal activity | Must implement back-channel logout |\n\n**Critical**: Configure access token lifetime to be shorter than the server-side session lifetime at IdentityServer, so that refresh token usage naturally keeps the session alive.\n\n## Session Expiration and Cleanup\n\nWhen a session cookie expires without explicit logout, the server-side session record remains in the store. An automatic cleanup job periodically scans for and removes these expired records.\n\n### Expiration Configuration Options\n\nAll options are on `options.ServerSideSessions`:\n\n| Option | Default | Description |\n| ------ | ------- | ----------- |\n| `RemoveExpiredSessions` | `true` | Enables periodic cleanup of expired sessions |\n| `RemoveExpiredSessionsFrequency` | 10 minutes | How often the cleanup job runs |\n| `RemoveExpiredSessionsBatchSize` | 100 | Number of expired records removed per batch |\n| `ExpiredSessionsTriggerBackchannelLogout` | `true` | Send back-channel logout notifications when expired sessions are cleaned up |\n| `FuzzExpiredSessionRemovalStart` | `true` | Randomize the first cleanup run to avoid multi-instance conflicts |\n\n### Customizing the Cleanup Interval\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer(options => {\n options.ServerSideSessions.RemoveExpiredSessionsFrequency = TimeSpan.FromSeconds(60);\n}).AddServerSideSessions();\n```\n\n### Disabling Automatic Cleanup\n\n```csharp\nbuilder.Services.AddIdentityServer(options => {\n options.ServerSideSessions.RemoveExpiredSessions = false;\n}).AddServerSideSessions();\n```\n\n### Configuring Session Lifetime\n\nThe server-side session lifetime is inherited from the cookie authentication handler:\n\n- **Default (no ASP.NET Identity)**: Controlled by `options.Authentication.CookieLifetime` (defaults to 10 hours)\n- **With ASP.NET Core Identity**: Controlled by `ConfigureApplicationCookie(options => options.ExpireTimeSpan = ...)` (defaults to 14 days)\n\n### Session Renewal and Absolute Lifetime Cap\n\nWith server-side sessions the **cookie expiration can extend beyond the configured lifetime**: IdentityServer calls `SignInAsync` whenever the session's client list changes (e.g. the user signs into an additional client), re-issuing the cookie and resetting its timer. Without server-side sessions, the cookie expiration is set once at login.\n\nTo enforce an absolute cap, combine:\n\n- `Client.UserSsoLifetime` — forces interactive re-authentication after N seconds, regardless of cookie renewals.\n- `Client.AbsoluteRefreshTokenLifetime` with `RefreshTokenExpiration = TokenExpiration.Absolute` — caps refresh-token-driven session extension.\n\n```csharp\nvar client = new Client\n{\n ClientId = \"web.app\",\n UserSsoLifetime = 8 * 3600, // re-auth after 8h\n AbsoluteRefreshTokenLifetime = 8 * 3600,\n RefreshTokenExpiration = TokenExpiration.Absolute,\n};\n```\n\n## Dynamic Identity Providers\n\n### Edition Requirements\n\nDynamic identity providers are part of the **Duende IdentityServer Enterprise Edition**.\n\n### Problem Statement\n\nStatically registering many authentication handlers via `AddOpenIdConnect()` has performance penalties in ASP.NET Core's DI system. It also requires application restart for configuration changes.\n\n### Solution\n\nDynamic providers are loaded from a store at runtime, avoiding DI overhead and enabling live configuration changes.\n\n### Store Options\n\n| Store | Implementation |\n| --------------------- | ---------------------------------- |\n| In-memory | `AddInMemoryIdentityProviders()` |\n| Entity Framework Core | Via `ConfigurationDbContext` |\n| Custom | Implement `IIdentityProviderStore` |\n\n### Adding a Dynamic OIDC Provider (In-Memory)\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer()\n .AddInMemoryIdentityProviders(new[]\n {\n new OidcProvider\n {\n Scheme = \"oidc\",\n DisplayName = \"Sample provider\",\n Enabled = true,\n // ... more properties\n }\n });\n```\n\n### Adding a Dynamic OIDC Provider (Entity Framework)\n\n```csharp\n// SeedData.cs\nprivate static async Task SeedDynamicProviders(ConfigurationDbContext context)\n{\n if (!context.IdentityProviders.Any())\n {\n context.IdentityProviders.Add(new OidcProvider\n {\n Scheme = \"demoidsrv\",\n DisplayName = \"IdentityServer (dynamic)\",\n Authority = \"https://demo.duendesoftware.com\",\n ClientId = \"login\",\n }.ToEntity());\n\n await context.SaveChangesAsync();\n }\n}\n```\n\n### Caching Dynamic Providers\n\nBy default, dynamic provider configuration is loaded from the store on every request. Enable caching:\n\n- **EF stores**: Use `AddConfigurationStoreCache()`\n- **Custom stores**: Use `AddIdentityProviderStoreCache<T>()`\n\n### Listing Dynamic Providers on the Login Page\n\nMerge static and dynamic providers:\n\n```csharp\n// Login.cshtml.cs\nvar schemes = await _schemeProvider.GetAllSchemesAsync();\n\nvar providers = schemes\n .Where(x => x.DisplayName != null)\n .Select(x => new ExternalProvider\n {\n DisplayName = x.DisplayName ?? x.Name,\n AuthenticationScheme = x.Name\n }).ToList();\n\nvar dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync())\n .Where(x => x.Enabled)\n .Select(x => new ExternalProvider\n {\n AuthenticationScheme = x.Scheme,\n DisplayName = x.DisplayName\n });\n\nproviders.AddRange(dynamicSchemes);\n```\n\n### Callback Path Convention\n\nDynamic providers follow the convention `~/federation/{scheme}/{suffix}`:\n\n| Path | Purpose |\n| --------------------------------------- | -------------------------------------------------- |\n| `/federation/{scheme}/signin` | OIDC redirect URI (`CallbackPath`) |\n| `/federation/{scheme}/signout-callback` | Post-logout redirect URI (`SignedOutCallbackPath`) |\n| `/federation/{scheme}/signout` | Front-channel logout URI (`RemoteSignOutPath`) |\n\nCustomize the prefix:\n\n```csharp\nbuilder.Services.AddIdentityServer(options =>\n{\n options.DynamicProviders.PathPrefix = \"/fed\";\n});\n```\n\n### Custom (Non-OIDC) Dynamic Providers\n\nTo add providers like Google or SAML:\n\n**Step 1**: Create a custom `IdentityProvider` type:\n\n```csharp\npublic class GoogleIdentityProvider : IdentityProvider\n{\n public const string ProviderType = \"google\";\n\n public GoogleIdentityProvider() : base(ProviderType) { }\n\n public string? ClientId\n {\n get => this[\"ClientId\"];\n set => this[\"ClientId\"] = value;\n }\n\n public string? ClientSecret\n {\n get => this[\"ClientSecret\"];\n set => this[\"ClientSecret\"] = value;\n }\n}\n```\n\n**Step 2**: Register the handler mapping:\n\n```csharp\n// Program.cs\nbuilder.Services.AddIdentityServer(options =>\n{\n options.DynamicProviders\n .AddProviderType<GoogleHandler, GoogleOptions, GoogleIdentityProvider>(\n GoogleIdentityProvider.ProviderType);\n});\n```\n\n**Step 3**: Configure options mapping:\n\n```csharp\nclass GoogleDynamicConfigureOptions\n : ConfigureAuthenticationOptions<GoogleOptions, GoogleIdentityProvider>\n{\n public GoogleDynamicConfigureOptions(IHttpContextAccessor httpContextAccessor,\n ILogger<GoogleDynamicConfigureOptions> logger) : base(httpContextAccessor, logger) { }\n\n protected override void Configure(\n ConfigureAuthenticationContext<GoogleOptions, GoogleIdentityProvider> context)\n {\n var googleProvider = context.IdentityProvider;\n var googleOptions = context.AuthenticationOptions;\n\n googleOptions.ClientId = googleProvider.ClientId;\n googleOptions.ClientSecret = googleProvider.ClientSecret;\n googleOptions.SignInScheme = context.DynamicProviderOptions.SignInScheme;\n googleOptions.CallbackPath = context.PathPrefix + \"/signin\";\n }\n}\n```\n\nRegister it:\n\n```csharp\nbuilder.Services.ConfigureOptions<GoogleDynamicConfigureOptions>();\n```\n\n### Customizing OpenIdConnectOptions for Dynamic Providers\n\nImplement `IConfigureNamedOptions<OpenIdConnectOptions>` for per-scheme customization:\n\n```csharp\npublic class CustomConfig : IConfigureNamedOptions<OpenIdConnectOptions>\n{\n public void Configure(string name, OpenIdConnectOptions options)\n {\n if (name == \"MyScheme\")\n {\n // customize options\n }\n }\n\n public void Configure(OpenIdConnectOptions options) { }\n}\n```\n\nRegister: `builder.Services.ConfigureOptions<CustomConfig>();`\n\nFor customizations that need access to the `OidcProvider` data (e.g., the `Properties` bag), derive from `ConfigureAuthenticationOptions<OpenIdConnectOptions, OidcProvider>` instead.\n\n## CIBA (Client Initiated Backchannel Authentication)\n\n### Edition Requirements\n\nCIBA is part of the **Duende IdentityServer Enterprise Edition**.\n\n### What Is CIBA?\n\nCIBA allows a user to authenticate on a different device than the one running the client application. Example: a user at a bank kiosk authenticates via their mobile phone.\n\n### CIBA Flow\n\n1. **Client** sends a backchannel authentication request to IdentityServer's `/connect/ciba` endpoint\n2. **IdentityServer** validates the request and identifies the user via `IBackchannelAuthenticationUserValidator` (you must implement this)\n3. **IdentityServer** creates a pending login request in the `IBackchannelAuthenticationRequestStore`\n4. **IdentityServer** notifies the user via `IBackchannelAuthenticationUserNotificationService` (you must implement this — e.g., push notification, email, SMS)\n5. **User** reviews and approves/denies the request; your UI calls `IBackchannelAuthenticationInteractionService.CompleteLoginRequestAsync`\n6. **Client** polls the token endpoint and receives tokens (or an error if denied/timed out)\n\n### Required Implementations\n\n| Interface | Your Responsibility |\n| --------------------------------------------------- | ------------------------------------------------------------------------------- |\n| `IBackchannelAuthenticationUserValidator` | Validate the request and return the user's `sub` claim |\n| `IBackchannelAuthenticationUserNotificationService` | Notify the user (push, email, SMS, etc.) with the `BackchannelUserLoginRequest` |\n\n### Client Configuration\n\n```csharp\nvar client = new Client\n{\n ClientId = \"kiosk.app\",\n AllowedGrantTypes = GrantTypes.Ciba, // CIBA grant\n // ClientSecrets, AllowedScopes, etc.\n};\n```\n\nThe client calls the backchannel authentication endpoint, receives an `auth_req_id`, then polls the token endpoint (`poll` mode). It must handle `authorization_pending`, `slow_down`, expiration, and user denial.\n\n### User Notification Service\n\n```csharp\npublic class UserNotificationService : IBackchannelAuthenticationUserNotificationService\n{\n public Task SendLoginRequestAsync(BackchannelUserLoginRequest request, CancellationToken ct)\n {\n // request.Subject.GetSubjectId() — the user to notify\n // request.InternalId — sensitive handle to the pending request\n // request.BindingMessage — show to user; compared on both devices\n // Deliver a push/SMS/email linking to your approval UI.\n return Task.CompletedTask;\n }\n}\n```\n\nRegister it:\n\n```csharp\nbuilder.Services.AddIdentityServer()\n .AddBackchannelAuthenticationUserNotificationService<UserNotificationService>();\n```\n\nThe built-in no-op implementation just logs a URL for testing — **replace it in production**. Treat `InternalId` as sensitive; never surface it in the notification. The user compares the `BindingMessage` shown on both the consumption device and their authentication device.\n\n### Approval UI\n\nUse `IBackchannelAuthenticationInteractionService`:\n\n```csharp\n// List this user's pending CIBA requests\nvar pending = await _cibaInteraction.GetPendingLoginRequestsForCurrentUserAsync(ct);\n\n// Reload one by internal id and verify ownership\nvar request = await _cibaInteraction.GetLoginRequestByInternalIdAsync(internalId, ct);\nif (request.Subject.GetSubjectId() != currentUserSubjectId) return Forbid();\n\n// Approve with consented scopes (a subset is allowed)\nawait _cibaInteraction.CompleteLoginRequestAsync(\n new CompleteBackchannelLoginRequest(internalId)\n {\n ScopesValuesConsented = request.ValidatedResources.RawScopeValues,\n }, ct);\nawait _events.RaiseAsync(new ConsentGrantedEvent(/* ... */));\n\n// Deny: leave ScopesValuesConsented empty/null\nawait _cibaInteraction.CompleteLoginRequestAsync(\n new CompleteBackchannelLoginRequest(internalId) { ScopesValuesConsented = null }, ct);\nawait _events.RaiseAsync(new ConsentDeniedEvent(/* ... */));\n```\n\nThe server **rejects any scope not present in the original CIBA request**. Raise `ConsentGrantedEvent` / `ConsentDeniedEvent` for audit.\n\nIdentityServer supports the `poll` mode for clients to obtain results.\n\n## Common Anti-Patterns\n\n- ❌ Using in-memory session store in production — sessions are lost on restart\n- ✅ Use Entity Framework Core or a custom durable store for production\n\n- ❌ Registering hundreds of static authentication handlers via `AddOpenIdConnect()`\n- ✅ Use dynamic identity providers for scalable provider management\n\n- ❌ Assuming inactivity timeout works automatically without enabling `CoordinateClientLifetimesWithUserSession`\n- ✅ Explicitly enable coordination at the global or per-client level\n\n- ❌ Using `QuerySessionsAsync` for simple session listing\n- ✅ Prefer `GetSessionsAsync` — it is faster; use `QuerySessionsAsync` only for advanced filtering\n\n- ❌ Forgetting to implement `IBackchannelAuthenticationUserValidator` and `IBackchannelAuthenticationUserNotificationService` for CIBA\n- ✅ Both interfaces must be implemented and registered in DI — IdentityServer does not provide defaults\n\n## Common Pitfalls\n\n1. **Registration order matters**: `AddServerSideSessions()` must be called after any custom `IRefreshTokenService` registration.\n\n2. **Data Protection dependency**: Server-side session data is protected using ASP.NET Core Data Protection. Ensure Data Protection keys are persisted and shared across load-balanced instances.\n\n3. **Session expiration vs cookie expiration**: The server-side session has its own lifetime. When a session expires server-side, the user's cookie becomes invalid even if the cookie itself hasn't expired.\n\n4. **Dynamic provider store is read-only**: `IIdentityProviderStore` only has query methods. To add/update/delete providers, use `ConfigurationDbContext` directly (for EF) or your own mechanism (for custom stores).\n\n5. **CIBA requires Enterprise Edition**: Attempting to use CIBA features without the Enterprise Edition license will fail at runtime.\n\n6. **Access token lifetime must be shorter than session timeout**: For inactivity timeout to work, refresh token usage must happen regularly enough to signal activity. If the access token lives longer than the session timeout, the client won't refresh in time.\n"
}SHA-256: c344785635a0d8026f9f7c14d2b381f7a613115247ee5f4ef3dfa90e6b9c5d34