Identityserver Sessions Providers

作者 DuendeSoftwarefb32edc51982無授權條款9 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫4 週前更新

Guide for configuring server-side sessions, session management and querying, inactivity timeout, dynamic identity providers, and CIBA (Client Initiated Backchannel Authentication) in Duende IdentityServer.

AI 產生的概覽

指導設定 Duende IdentityServer 的伺服器端工作階段、動態身分提供者與 CIBA 流程。

功能
此技能僅提供說明,講解如何在 Duende IdentityServer 中設定伺服器端工作階段,包括啟用工作階段、使用 Entity Framework Core 或自訂工作階段存放區、透過 ISessionManagementService 查詢與撤銷工作階段,以及設定閒置逾時與清理選項。內容也涵蓋執行階段載入的動態身分提供者(含自訂非 OIDC 提供者)以及 CIBA 反向通道驗證流程。產出為設定指引與程式碼片段,而非檔案或指令碼。
適用情境
適用於設定或排解 Duende IdentityServer 的驗證狀態管理,例如啟用伺服器端工作階段、讓工作階段存續期與用戶端權杖協調、新增動態身分提供者或實作 CIBA。也適合在確認某項功能需要哪個 IdentityServer 版本時使用。
執行需求
不含指令碼,僅為說明性內容。需要 Duende IdentityServer 專案(通常為 ASP.NET Core),並具備所述功能所需的 Business 或 Enterprise 版本授權;若需持久化工作階段或提供者存放區,還需要 Entity Framework Core。

IdentityServer Sessions, Dynamic Providers, and CIBA

When to Use This Skill

  • Enabling and configuring server-side sessions for authentication state management
  • Implementing session querying, revocation, and administrative tooling via ISessionManagementService
  • Configuring inactivity timeout across IdentityServer and client applications
  • Setting up the Entity Framework Core session store or implementing a custom IServerSideSessionStore
  • Adding dynamic identity providers loaded from a database at runtime
  • Implementing custom non-OIDC dynamic provider types (Google, SAML, etc.)
  • Building a CIBA (Client Initiated Backchannel Authentication) flow
  • Understanding edition requirements (Business vs Enterprise) for these features

Docs: https://docs.duendesoftware.com/identityserver/ui/server-side-sessions/

Server-Side Sessions

What Problem Do They Solve?

By default, ASP.NET Core stores all authentication session state in a self-contained cookie. This creates several challenges:

ProblemImpact
Cookie size growthAs clients are tracked, the cookie grows; large cookies can exceed browser limits
No session visibilityCannot query how many active sessions exist
No administrative revocationCannot terminate a session from outside the user's browser
No server-side coordinationCannot detect inactivity or synchronize session expiration across clients

Server-side sessions store authentication state on the server, keeping only a session reference in the cookie.

Edition Requirements

Server-side sessions are part of the Duende IdentityServer Business and Enterprise Edition.

Enabling Server-Side Sessions

csharp
// Program.csbuilder.Services.AddIdentityServer()    .AddServerSideSessions();

Important: This call must come after any custom IRefreshTokenService implementation registration. Order matters in the ASP.NET Core service provider.

By default, sessions are stored in-memory. For production, use Entity Framework Core or a custom store.

Using Entity Framework Core Store

csharp
// Program.csbuilder.Services.AddIdentityServer()    .AddServerSideSessions()    .AddOperationalStore(options =>    {        options.ConfigureDbContext = builder =>            builder.UseSqlServer(connectionString,                sql => sql.MigrationsAssembly(migrationsAssembly));    });

The EF Core implementation is included in the operational store and supports the IServerSideSessionStore interface automatically.

Custom Session Store

Implement IServerSideSessionStore and register it:

csharp
// Program.cs — two-step registrationbuilder.Services.AddIdentityServer()    .AddServerSideSessions()    .AddServerSideSessionStore<YourCustomStore>();
// Or one-step registrationbuilder.Services.AddIdentityServer()    .AddServerSideSessions<YourCustomStore>();

Data Stored Server-Side

The session stores the serialized ASP.NET Core AuthenticationTicket (all claims + AuthenticationProperties.Items). The data is protected using ASP.NET Core's Data Protection API.

Queryable indices extracted from the session:

IndexSource
Subject IDsub claim value
Session IDsid claim value
Display NameConfigurable claim type (e.g., name or email)

Configure 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:

csharp
// Program.csbuilder.Services.AddIdentityServer(options => {    options.ServerSideSessions.UserDisplayNameClaimType = "name";}).AddServerSideSessions();

Session Management with ISessionManagementService

Querying Sessions

csharp
var userSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery{    CountRequested = 10,    SubjectId = "12345",    DisplayName = "Bob",});

Paging Through Results

csharp
// First pagevar userSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery{    CountRequested = 10,});
// Next pageuserSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery{    ResultsToken = userSessions.ResultsToken,    CountRequested = 10,});
// Previous pageuserSessions = await _sessionManagementService.QuerySessionsAsync(new SessionQuery{    ResultsToken = userSessions.ResultsToken,    RequestPriorResults = true,    CountRequested = 10,});

Performance Note on Querying

When 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.

Terminating Sessions

Terminate sessions and optionally revoke tokens, consents, and send back-channel logout notifications:

csharp
// Revoke everything for a userawait _sessionManagementService.RemoveSessionsAsync(new RemoveSessionsContext{    SubjectId = "12345"});

Selective revocation (filtering by SessionId or ClientIds is also supported):

csharp
// Only revoke refresh tokens, keep session and consentsawait _sessionManagementService.RemoveSessionsAsync(new RemoveSessionsContext{    SubjectId = "12345",    SessionId = "abc123",        // optional: target a specific session    ClientIds = { "my_app" },    // optional: target specific clients    RevokeTokens = true,    RemoveServerSideSession = false,    RevokeConsents = false,    SendBackchannelLogoutNotification = false,});

What Gets Cleaned Up

FlagEffect
RemoveServerSideSession (default: true)Deletes the session record from the store
RevokeTokens (default: true)Revokes refresh tokens and reference access tokens
RevokeConsents (default: true)Removes persisted consent grants
SendBackchannelLogoutNotification (default: true)Sends back-channel logout to clients with BackChannelLogoutUri

Internally, this uses IServerSideTicketStore, IPersistedGrantStore, and IBackChannelLogoutService.

Server-Side Session Custom Metadata

Store 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.

Writing Metadata

csharp
var properties = new AuthenticationProperties();properties.Items["device_name"] = "Bob's iPhone";properties.Items["region"] = "eu-west";
await HttpContext.SignInAsync(identityServerUser, properties);

With ASP.NET Identity, pass properties to SignInWithClaimsAsync:

csharp
await _signInManager.SignInWithClaimsAsync(user, properties, additionalClaims: []);

If you use PasswordSignInAsync (which does not accept properties), override SignInWithClaimsAsync in a custom SignInManager to inject the metadata.

Reading Metadata

csharp
var sessions = await _sessionManagementService.QuerySessionsAsync(    new SessionQuery { SubjectId = "12345" });
foreach (var session in sessions.Results){    if (session.AuthenticationTicket.Properties.Items        .TryGetValue("device_name", out var deviceName))    {        // use deviceName    }}

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.

Inactivity Timeout

The Challenge

OpenID 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.

Design: Centralized Session Tracking

Server-side sessions at IdentityServer provide the central record for monitoring user activity:

  1. 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).
  2. Inactivity detection: When no activity occurs within the session timeout, the session expires and cleanup is triggered (back-channel logout, token revocation).

Configuration at IdentityServer

Three features must be enabled:

csharp
// Program.csbuilder.Services.AddIdentityServer(options =>{    // 1. Enable server-side sessions    // (done separately via .AddServerSideSessions())
    // 2. Coordinate client token lifetimes with the user session    options.Authentication.CoordinateClientLifetimesWithUserSession = true;
    // 3. Trigger back-channel logout when sessions expire    // This is already true by default, shown here for explicitness    options.ServerSideSessions.ExpiredSessionsTriggerBackchannelLogout = true;}).AddServerSideSessions();

Note: ExpiredSessionsTriggerBackchannelLogout defaults to true, so step 3 is technically optional. The only setting you must explicitly enable is CoordinateClientLifetimesWithUserSession (step 2).

Alternatively, enable coordination per-client:

csharp
var client = new Client{    ClientId = "my_app",    CoordinateLifetimeWithUserSession = true};

Client-Side Configuration

Client TypeHow Activity Is SignaledHow Inactivity Is Detected
Client with refresh tokensRefresh token requests extend the sessionHandle refresh token failure, or implement back-channel logout
Client with reference tokens (no refresh)Introspection extends the sessionHandle 401 from API, or implement back-channel logout
Client without access tokensCannot signal activityMust implement back-channel logout

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.

Session Expiration and Cleanup

When 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.

Expiration Configuration Options

All options are on options.ServerSideSessions:

OptionDefaultDescription
RemoveExpiredSessionstrueEnables periodic cleanup of expired sessions
RemoveExpiredSessionsFrequency10 minutesHow often the cleanup job runs
RemoveExpiredSessionsBatchSize100Number of expired records removed per batch
ExpiredSessionsTriggerBackchannelLogouttrueSend back-channel logout notifications when expired sessions are cleaned up
FuzzExpiredSessionRemovalStarttrueRandomize the first cleanup run to avoid multi-instance conflicts

Customizing the Cleanup Interval

csharp
// Program.csbuilder.Services.AddIdentityServer(options => {    options.ServerSideSessions.RemoveExpiredSessionsFrequency = TimeSpan.FromSeconds(60);}).AddServerSideSessions();

Disabling Automatic Cleanup

csharp
builder.Services.AddIdentityServer(options => {    options.ServerSideSessions.RemoveExpiredSessions = false;}).AddServerSideSessions();

Configuring Session Lifetime

The server-side session lifetime is inherited from the cookie authentication handler:

  • Default (no ASP.NET Identity): Controlled by options.Authentication.CookieLifetime (defaults to 10 hours)
  • With ASP.NET Core Identity: Controlled by ConfigureApplicationCookie(options => options.ExpireTimeSpan = ...) (defaults to 14 days)

Session Renewal and Absolute Lifetime Cap

With 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.

To enforce an absolute cap, combine:

  • Client.UserSsoLifetime — forces interactive re-authentication after N seconds, regardless of cookie renewals.
  • Client.AbsoluteRefreshTokenLifetime with RefreshTokenExpiration = TokenExpiration.Absolute — caps refresh-token-driven session extension.
csharp
var client = new Client{    ClientId = "web.app",    UserSsoLifetime = 8 * 3600,                 // re-auth after 8h    AbsoluteRefreshTokenLifetime = 8 * 3600,    RefreshTokenExpiration = TokenExpiration.Absolute,};

Dynamic Identity Providers

Edition Requirements

Dynamic identity providers are part of the Duende IdentityServer Enterprise Edition.

Problem Statement

Statically registering many authentication handlers via AddOpenIdConnect() has performance penalties in ASP.NET Core's DI system. It also requires application restart for configuration changes.

Solution

Dynamic providers are loaded from a store at runtime, avoiding DI overhead and enabling live configuration changes.

Store Options

StoreImplementation
In-memoryAddInMemoryIdentityProviders()
Entity Framework CoreVia ConfigurationDbContext
CustomImplement IIdentityProviderStore

Adding a Dynamic OIDC Provider (In-Memory)

csharp
// Program.csbuilder.Services.AddIdentityServer()    .AddInMemoryIdentityProviders(new[]    {        new OidcProvider        {            Scheme = "oidc",            DisplayName = "Sample provider",            Enabled = true,            // ... more properties        }    });

Adding a Dynamic OIDC Provider (Entity Framework)

csharp
// SeedData.csprivate static async Task SeedDynamicProviders(ConfigurationDbContext context){    if (!context.IdentityProviders.Any())    {        context.IdentityProviders.Add(new OidcProvider        {            Scheme = "demoidsrv",            DisplayName = "IdentityServer (dynamic)",            Authority = "https://demo.duendesoftware.com",            ClientId = "login",        }.ToEntity());
        await context.SaveChangesAsync();    }}

Caching Dynamic Providers

By default, dynamic provider configuration is loaded from the store on every request. Enable caching:

  • EF stores: Use AddConfigurationStoreCache()
  • Custom stores: Use AddIdentityProviderStoreCache<T>()

Listing Dynamic Providers on the Login Page

Merge static and dynamic providers:

csharp
// Login.cshtml.csvar schemes = await _schemeProvider.GetAllSchemesAsync();
var providers = schemes    .Where(x => x.DisplayName != null)    .Select(x => new ExternalProvider    {        DisplayName = x.DisplayName ?? x.Name,        AuthenticationScheme = x.Name    }).ToList();
var dynamicSchemes = (await _identityProviderStore.GetAllSchemeNamesAsync())    .Where(x => x.Enabled)    .Select(x => new ExternalProvider    {        AuthenticationScheme = x.Scheme,        DisplayName = x.DisplayName    });
providers.AddRange(dynamicSchemes);

Callback Path Convention

Dynamic providers follow the convention ~/federation/{scheme}/{suffix}:

PathPurpose
/federation/{scheme}/signinOIDC redirect URI (CallbackPath)
/federation/{scheme}/signout-callbackPost-logout redirect URI (SignedOutCallbackPath)
/federation/{scheme}/signoutFront-channel logout URI (RemoteSignOutPath)

Customize the prefix:

csharp
builder.Services.AddIdentityServer(options =>{    options.DynamicProviders.PathPrefix = "/fed";});

Custom (Non-OIDC) Dynamic Providers

To add providers like Google or SAML:

Step 1: Create a custom IdentityProvider type:

csharp
public class GoogleIdentityProvider : IdentityProvider{    public const string ProviderType = "google";
    public GoogleIdentityProvider() : base(ProviderType) { }
    public string? ClientId    {        get => this["ClientId"];        set => this["ClientId"] = value;    }
    public string? ClientSecret    {        get => this["ClientSecret"];        set => this["ClientSecret"] = value;    }}

Step 2: Register the handler mapping:

csharp
// Program.csbuilder.Services.AddIdentityServer(options =>{    options.DynamicProviders        .AddProviderType<GoogleHandler, GoogleOptions, GoogleIdentityProvider>(            GoogleIdentityProvider.ProviderType);});

Step 3: Configure options mapping:

csharp
class GoogleDynamicConfigureOptions    : ConfigureAuthenticationOptions<GoogleOptions, GoogleIdentityProvider>{    public GoogleDynamicConfigureOptions(IHttpContextAccessor httpContextAccessor,        ILogger<GoogleDynamicConfigureOptions> logger) : base(httpContextAccessor, logger) { }
    protected override void Configure(        ConfigureAuthenticationContext<GoogleOptions, GoogleIdentityProvider> context)    {        var googleProvider = context.IdentityProvider;        var googleOptions = context.AuthenticationOptions;
        googleOptions.ClientId = googleProvider.ClientId;        googleOptions.ClientSecret = googleProvider.ClientSecret;        googleOptions.SignInScheme = context.DynamicProviderOptions.SignInScheme;        googleOptions.CallbackPath = context.PathPrefix + "/signin";    }}

Register it:

csharp
builder.Services.ConfigureOptions<GoogleDynamicConfigureOptions>();

Customizing OpenIdConnectOptions for Dynamic Providers

Implement IConfigureNamedOptions<OpenIdConnectOptions> for per-scheme customization:

csharp
public class CustomConfig : IConfigureNamedOptions<OpenIdConnectOptions>{    public void Configure(string name, OpenIdConnectOptions options)    {        if (name == "MyScheme")        {            // customize options        }    }
    public void Configure(OpenIdConnectOptions options) { }}

Register: builder.Services.ConfigureOptions<CustomConfig>();

For customizations that need access to the OidcProvider data (e.g., the Properties bag), derive from ConfigureAuthenticationOptions<OpenIdConnectOptions, OidcProvider> instead.

CIBA (Client Initiated Backchannel Authentication)

Edition Requirements

CIBA is part of the Duende IdentityServer Enterprise Edition.

What Is CIBA?

CIBA 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.

CIBA Flow

  1. Client sends a backchannel authentication request to IdentityServer's /connect/ciba endpoint
  2. IdentityServer validates the request and identifies the user via IBackchannelAuthenticationUserValidator (you must implement this)
  3. IdentityServer creates a pending login request in the IBackchannelAuthenticationRequestStore
  4. IdentityServer notifies the user via IBackchannelAuthenticationUserNotificationService (you must implement this — e.g., push notification, email, SMS)
  5. User reviews and approves/denies the request; your UI calls IBackchannelAuthenticationInteractionService.CompleteLoginRequestAsync
  6. Client polls the token endpoint and receives tokens (or an error if denied/timed out)

Required Implementations

InterfaceYour Responsibility
IBackchannelAuthenticationUserValidatorValidate the request and return the user's sub claim
IBackchannelAuthenticationUserNotificationServiceNotify the user (push, email, SMS, etc.) with the BackchannelUserLoginRequest

Client Configuration

csharp
var client = new Client{    ClientId = "kiosk.app",    AllowedGrantTypes = GrantTypes.Ciba,   // CIBA grant    // ClientSecrets, AllowedScopes, etc.};

The 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.

User Notification Service

csharp
public class UserNotificationService : IBackchannelAuthenticationUserNotificationService{    public Task SendLoginRequestAsync(BackchannelUserLoginRequest request, CancellationToken ct)    {        // request.Subject.GetSubjectId() — the user to notify        // request.InternalId            — sensitive handle to the pending request        // request.BindingMessage        — show to user; compared on both devices        // Deliver a push/SMS/email linking to your approval UI.        return Task.CompletedTask;    }}

Register it:

csharp
builder.Services.AddIdentityServer()    .AddBackchannelAuthenticationUserNotificationService<UserNotificationService>();

The 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.

Approval UI

Use IBackchannelAuthenticationInteractionService:

csharp
// List this user's pending CIBA requestsvar pending = await _cibaInteraction.GetPendingLoginRequestsForCurrentUserAsync(ct);
// Reload one by internal id and verify ownershipvar request = await _cibaInteraction.GetLoginRequestByInternalIdAsync(internalId, ct);if (request.Subject.GetSubjectId() != currentUserSubjectId) return Forbid();
// Approve with consented scopes (a subset is allowed)await _cibaInteraction.CompleteLoginRequestAsync(    new CompleteBackchannelLoginRequest(internalId)    {        ScopesValuesConsented = request.ValidatedResources.RawScopeValues,    }, ct);await _events.RaiseAsync(new ConsentGrantedEvent(/* ... */));
// Deny: leave ScopesValuesConsented empty/nullawait _cibaInteraction.CompleteLoginRequestAsync(    new CompleteBackchannelLoginRequest(internalId) { ScopesValuesConsented = null }, ct);await _events.RaiseAsync(new ConsentDeniedEvent(/* ... */));

The server rejects any scope not present in the original CIBA request. Raise ConsentGrantedEvent / ConsentDeniedEvent for audit.

IdentityServer supports the poll mode for clients to obtain results.

Common Anti-Patterns

  • ❌ Using in-memory session store in production — sessions are lost on restart

  • ✅ Use Entity Framework Core or a custom durable store for production

  • ❌ Registering hundreds of static authentication handlers via AddOpenIdConnect()

  • ✅ Use dynamic identity providers for scalable provider management

  • ❌ Assuming inactivity timeout works automatically without enabling CoordinateClientLifetimesWithUserSession

  • ✅ Explicitly enable coordination at the global or per-client level

  • ❌ Using QuerySessionsAsync for simple session listing

  • ✅ Prefer GetSessionsAsync — it is faster; use QuerySessionsAsync only for advanced filtering

  • ❌ Forgetting to implement IBackchannelAuthenticationUserValidator and IBackchannelAuthenticationUserNotificationService for CIBA

  • ✅ Both interfaces must be implemented and registered in DI — IdentityServer does not provide defaults

Common Pitfalls

  1. Registration order matters: AddServerSideSessions() must be called after any custom IRefreshTokenService registration.

  2. 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.

  3. 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.

  4. 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).

  5. CIBA requires Enterprise Edition: Attempting to use CIBA features without the Enterprise Edition license will fail at runtime.

  6. 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.

來源與署名

來源:DuendeSoftware/duende-skills位於skills/identityserver-sessions-providers提交fb32edc

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架