Identity Testing Patterns
When to Use This Skill
Use this skill when:
- Writing integration tests for applications that issue or validate tokens using Duende IdentityServer
- Hosting IdentityServer in-memory with
WebApplicationFactory<T>to test grant flows end-to-end - Creating mock JWT tokens for testing protected APIs without a live authority
- Testing custom
IProfileServiceimplementations or claim transformation logic - Verifying
IAuthorizationHandlerand policy-based authorization against specific claim sets - Testing BFF endpoints that rely on cookie-based sessions and proxied API calls
- Scaffolding an integration-test project from the
duende-is-inmemtemplate to test IdentityServer itself - Running a post-deployment login-flow smoke test without a headless browser
Core Principles
- Integration over unit — Test token issuance, claim mapping, and policy enforcement against a real (in-process) IdentityServer instance. Avoid mocking the token pipeline itself; mock only external I/O (databases, downstream services).
- In-process authority — Use
WebApplicationFactory<T>to host IdentityServer inside the test process. This avoids network round-trips, eliminates certificate trust issues, and makes tests deterministic. - Predictable signing keys — Override key management in tests with a static development signing key so token signatures are verifiable without key rotation logic.
- Minimal test clients — Register only the clients, scopes, and resources each test needs. Over-broad test configurations mask permission bugs.
- Test auth handler for API tests — When testing protected APIs in isolation (without a live token endpoint), replace JWT Bearer authentication with a
TestAuthHandlerthat accepts a fake scheme. Never disable authorization wholesale. - Builder pattern for test data — Use fluent builders for
Client,ApiScope,ApiResource, and test users to keep test setup readable and reduce duplication.
Related Skills
identityserver-configuration— Production client and resource registration patternsaspnetcore-authentication— OIDC and JWT Bearer handler configurationaspnetcore-authorization— Policy definitions and requirement handlersclaims-authorization—IProfileServiceand claim pipeline internalsduende-bff— BFF session and proxy architecture being tested
Docs: https://docs.duendesoftware.com/identityserver/fundamentals
Sub-Documents
Testing Strategy Overview
Pattern 1: WebApplicationFactory for IdentityServer
Host a complete IdentityServer in-memory. Override configuration to inject test clients, resources, and a static signing key.
Required NuGet Packages
IdentityServer WebApplicationFactory
Requesting a Token in a Test
Testing IdentityServer Itself from the duende-is-inmem Template
The fastest way to get a real, in-process IdentityServer under test is to scaffold from the in-memory template duende-is-inmem (from the Duende.Templates NuGet package) and add a test project that references the host. Unlike the TestAuthHandler/TestTokenFactory patterns below — which mock auth in a downstream API — this exercises IdentityServer's real endpoints and token issuance (no auth mocking).
Test project setup:
- The test project must use the Web SDK so ASP.NET Core testing APIs resolve. Change the top of the
.csproj:
- Add packages to the test project:
-
Reference the IdentityServer host project (
<ProjectReference Include="..\IdentityServerHost\IdentityServerHost.csproj" />). -
Make the template's static
Configcollections mutable so tests can add/clear clients and scopes:
Caution: With xUnit's parallel test execution, mutating shared static
Configcollections causes cross-test interference. Prefer per-test collections (or a fresh factory per test) over mutating shared statics.
Test class using the primary-constructor IClassFixture:
The discovery/token helpers (
GetDiscoveryDocumentAsync,RequestClientCredentialsTokenAsync) come fromDuende.IdentityModel. Accessingfactory.Serviceslets you assert on real in-process services such asIProfileService, stores, or options.
Pattern 2: Test Configuration Builders
Use static builders — not scattered inline literals — so every test builds from a consistent baseline.
Client Builder
TestUser Builder
Pattern 3: Mock Token Issuance
When testing a protected API in isolation (no live IdentityServer needed), issue a self-signed JWT in the test and configure the API to trust it. This avoids spinning up an IdentityServer host for every API test.
Generating a Self-Signed Test Token
Configuring the API to Trust the Test Token
Using the Test Token in a Test
Pattern 4: TestAuthHandler for Protected API Tests
For APIs that use [Authorize], replace the authentication handler entirely with a TestAuthHandler that accepts any pre-built ClaimsPrincipal. This gives full control over identity in each test without token serialization.
Factory Registration
Test Using the Handler
Pattern 5: Testing IProfileService
Unit test IProfileService implementations directly against the ProfileDataRequestContext contract. Use real ProfileDataRequestContext instances — do not mock the context.
Note:
ProfileDataRequestContextandIsActiveContextconstructors are internal to Duende IdentityServer in some versions. If the constructors are inaccessible, test through the in-processWebApplicationFactoryby issuing a real token and inspecting its claims withJsonWebTokenHandler.
Pattern 6: Testing Authorization Policies
Unit Testing an IAuthorizationHandler
Test IAuthorizationHandler implementations in isolation by constructing AuthorizationHandlerContext with synthetic claims.
Integration Testing Policy Enforcement
Verify that policies enforce correctly against real endpoints using TestAuthHandler:
Pattern 7: Testing BFF Endpoints
BFF tests require cookie-based session simulation using CookieContainer + HttpClientHandler. Set AllowAutoRedirect = false so session redirects don't swallow status codes. Include x-csrf: 1 header on all BFF local API calls — missing it returns 400. Override the OIDC OnRedirectToIdentityProvider event to bypass external redirects in tests.
See docs/bff-testing.md [blocked] for the complete
BffFactory,CookieContainersetup, and antiforgery header test examples.
Pattern 8: Testing with Aspire (Full-Stack)
Wire IdentityServer as a named Aspire resource, then use WaitForResourceHealthyAsync("idp", cts.Token) before requesting tokens. Obtain idp endpoint via _app.GetEndpoint("idp", "https") and pass it to RequestClientCredentialsTokenAsync.
See docs/aspire-testing.md [blocked] for the complete AppHost wiring and test fixture setup.
Pattern 9: Validating Issued Token Claims
After issuing a token through the in-process IdentityServer, parse the JWT and assert on its claims without making a separate network call.
Pattern 10: Post-Deployment Login Smoke Test (No Headless Browser)
Verify a real login flow against a deployed environment without Playwright/Selenium by driving a cookie-aware HttpClient and parsing HTML with AngleSharp. This exercises the interactive authorize → login-form → post-back → redirect-back chain end to end.
Field names (
Username,Password,__RequestVerificationToken,button="login") assume the default template login form. Adjust selectors if you customized the login UI. This is a smoke test — it confirms the deployed flow works, not per-claim correctness (use the in-process patterns above for that).
Common Pitfalls
1. Not Disabling Automatic Key Management in Tests
2. Disabling Authorization Entirely in Tests
3. Hard-Coding Localhost Ports
4. Forgetting to Add the openid Scope for Interactive Flows
5. Sharing a Single HttpClient Across Tests with TestAuthHandler
6. Not Awaiting Token Endpoint During Aspire Startup
7. Incorrect Audience in Self-Signed Test Tokens
Resources
- Duende IdentityServer Quickstarts
- Duende IdentityServer Samples — GitHub
- ASP.NET Core Integration Tests with WebApplicationFactory
- IProfileService Reference — Duende Docs
- Protecting APIs with JWT — Duende Docs
- ASP.NET Core Authorization Tests — Microsoft Docs
- IdentityModel Client Library
- Duende BFF Samples


