Claude
Skills
Sign in
Back

identity-testing-patterns

Included with Lifetime
$97 forever

Testing patterns for IdentityServer-based systems including integration testing with WebApplicationFactory, mock token issuance, test authority configuration, protocol response validation, and end-to-end authentication flow testing.

Code Review

What this skill does


# 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 `IProfileService` implementations or claim transformation logic
- Verifying `IAuthorizationHandler` and policy-based authorization against specific claim sets
- Testing BFF endpoints that rely on cookie-based sessions and proxied API calls

## Core Principles

1. **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).
2. **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.
3. **Predictable signing keys** — Override key management in tests with a static development signing key so token signatures are verifiable without key rotation logic.
4. **Minimal test clients** — Register only the clients, scopes, and resources each test needs. Over-broad test configurations mask permission bugs.
5. **Test auth handler for API tests** — When testing protected APIs in isolation (without a live token endpoint), replace JWT Bearer authentication with a `TestAuthHandler` that accepts a fake scheme. Never disable authorization wholesale.
6. **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 patterns
- `aspnetcore-authentication` — OIDC and JWT Bearer handler configuration
- `aspnetcore-authorization` — Policy definitions and requirement handlers
- `claims-authorization` — `IProfileService` and claim pipeline internals
- `duende-bff` — BFF session and proxy architecture being tested

Docs: https://docs.duendesoftware.com/identityserver/fundamentals

---

## Sub-Documents

| Document | Description | When to Load |
|----------|-------------|--------------|
| [docs/bff-testing.md](docs/bff-testing.md) | BFF endpoint testing with cookie simulation, antiforgery headers, and OIDC redirect bypass | BFF testing, CookieContainer, x-csrf header, BffFactory, session simulation |
| [docs/aspire-testing.md](docs/aspire-testing.md) | Full-stack Aspire testing with identity server health checks and token endpoint wiring | Aspire testing, DistributedApplicationTestingBuilder, WaitForResourceHealthyAsync, end-to-end |

---

## Testing Strategy Overview

| What to test | Recommended approach |
|---|---|
| Token issuance (client credentials, code flow) | In-process `WebApplicationFactory` hitting `/connect/token` |
| Claim mapping / `IProfileService` | Unit test with `DefaultProfileService` + mock context, or integration test |
| Authorization policy requirements | `IAuthorizationService` + `TestAuthHandler` in integration test |
| `IAuthorizationHandler` logic | Direct unit test with `AuthorizationHandlerContext` |
| Protected API access control | `WebApplicationFactory` with `TestAuthHandler` and constructed `ClaimsPrincipal` |
| BFF endpoints (login/logout/user) | `WebApplicationFactory` with cookie simulation |
| EF Core store implementations | In-memory EF provider or isolated SQL container |

---

## 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

```xml
<ItemGroup>
  <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="*" />
  <PackageReference Include="xunit" Version="*" />
  <PackageReference Include="xunit.runner.visualstudio" Version="*" />
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
  <PackageReference Include="IdentityModel" Version="*" />
</ItemGroup>
```

### IdentityServer WebApplicationFactory

```csharp
// ✅ Factory that runs a real IdentityServer in-process
public sealed class IdentityServerFactory : WebApplicationFactory<Program>
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.UseEnvironment("Testing");

        builder.ConfigureTestServices(services =>
        {
            // Remove any existing IdentityServer registration to replace it cleanly
            var descriptor = services.SingleOrDefault(
                d => d.ServiceType == typeof(IConfigureOptions<IdentityServerOptions>));
            if (descriptor is not null)
                services.Remove(descriptor);

            services.AddIdentityServer(options =>
            {
                options.Events.RaiseErrorEvents = true;
                options.Events.RaiseFailureEvents = true;

                // Disable automatic key management — use a static key for predictability
                options.KeyManagement.Enabled = false;
            })
            .AddInMemoryClients(TestConfig.Clients)
            .AddInMemoryApiScopes(TestConfig.ApiScopes)
            .AddInMemoryApiResources(TestConfig.ApiResources)
            .AddInMemoryIdentityResources(TestConfig.IdentityResources)
            .AddTestUsers(TestConfig.Users)
            // Static development signing key — never use this in production
            .AddDeveloperSigningCredential(persistKey: false);
        });
    }
}
```

### Requesting a Token in a Test

```csharp
[Collection("IdentityServer")]
public class TokenEndpointTests : IClassFixture<IdentityServerFactory>
{
    private readonly HttpClient _client;

    public TokenEndpointTests(IdentityServerFactory factory)
    {
        _client = factory.CreateClient();
    }

    [Fact]
    public async Task ClientCredentials_ShouldReturnAccessToken()
    {
        var response = await _client.RequestClientCredentialsTokenAsync(
            new ClientCredentialsTokenRequest
            {
                Address = "https://localhost/connect/token",
                ClientId = "test.service",
                ClientSecret = "test-secret",
                Scope = "api1"
            });

        Assert.False(response.IsError, response.Error);
        Assert.NotEmpty(response.AccessToken);
        Assert.Equal("Bearer", response.TokenType);
    }

    [Fact]
    public async Task ClientCredentials_InvalidScope_ShouldReturnError()
    {
        var response = await _client.RequestClientCredentialsTokenAsync(
            new ClientCredentialsTokenRequest
            {
                Address = "https://localhost/connect/token",
                ClientId = "test.service",
                ClientSecret = "test-secret",
                Scope = "not.allowed"  // ❌ scope not granted to this client
            });

        Assert.True(response.IsError);
        Assert.Equal("invalid_scope", response.Error);
    }
}
```

---

## Pattern 2: Test Configuration Builders

Use static builders — not scattered inline literals — so every test builds from a consistent baseline.

```csharp
public static class TestConfig
{
    public static IEnumerable<Client> Clients =>
    [
        ClientBuilder.ClientCredentials("test.service", "test-secret")
            .WithScopes("api1", "api2.read")
            .Build(),

        ClientBuilder.AuthorizationCode("test.webapp", "webapp-secret")
            .WithRedirectUri("https://testapp/signin-oidc")
            .WithScopes("openid", "profile", "api1")
            .Build()
    ];

    public static IEnumerable<ApiScope> ApiScopes =>
    [
        new ApiScope("api1", "Primary API"),
        new ApiScope("api2.read", "Read from API 2")
    ];

    public static IEnumerable<ApiResource> ApiResources =>
    [
        new ApiResource("ap

Related in Code Review