Integration Testing with Duende IdentityServer
Testing an application that relies on OAuth 2.0 and OpenID Connect can be intimidating. There are tokens to acquire, discovery documents to read, and a login flow that spans several redirects. You can avoid most of that friction by running a real, fully configured Duende IdentityServer in memory as part of your test suite and calling it like any other ASP.NET Core application.
This page shows how to stand up an in-process IdentityServer for integration testing, request tokens, and change configuration per test. For a lightweight way to smoke test the login flow of an instance you have already deployed, see Smoke Testing a Deployed Login Flow.
Why Run Integration Tests In-Process?
Section titled “Why Run Integration Tests In-Process?”Most teams treat their identity provider like an appliance. It sits in the background doing critical work, and you rarely think about it until something breaks. Running an in-process instance during tests gives you accurate OAuth 2.0 and OpenID Connect behavior with real endpoints, real token issuance, and real validation. You get that without external infrastructure or a headless browser.
In-process tests are a good fit when you want to verify that:
- Your IdentityServer configuration (clients, scopes, resources) is correct
- Your APIs accept tokens issued by your IdentityServer
- Any customizations, such as profile services, extension grants, or validators, behave as intended
How to Set Up the Test Solution
Section titled “How to Set Up the Test Solution”Start from the in-memory template, duende-is-inmem, in the
Duende.Templates NuGet package. As the name suggests,
its configuration and users live in memory, which makes it a flexible starting point for tests.
You need at least two projects: your IdentityServer host, and a test project. This example uses xUnit, but any test framework works.
To let the test project run the host as a test server, add a reference from the test project to the host project.
In the test project, switch to the web SDK so the ASP.NET Core testing APIs are available:
<Project Sdk="Microsoft.NET.Sdk"><Project Sdk="Microsoft.NET.Sdk.Web">Then add the Microsoft.AspNetCore.Mvc.Testing
package, which provides WebApplicationFactory<T> for running the host in memory:
dotnet add package Microsoft.AspNetCore.Mvc.TestingYou will likely also want to use the Duende.IdentityModel package in your test project, to make consuming OpenID Connect and OAuth 2.0 endpoints more straightforward:
dotnet add package Duende.IdentityModelHow to Make Configuration Modifiable at Runtime
Section titled “How to Make Configuration Modifiable at Runtime”The template ships a static Config class that holds the in-memory clients, scopes, and resources.
For testing, make its collections mutable so a single test can add a purpose-built client or scope:
public static class Config{ public static List<IdentityResource> IdentityResources { get; } = [ new IdentityResources.OpenId(), new IdentityResources.Profile(), ];
public static List<ApiScope> ApiScopes { get; } = [ new ApiScope("api1", "My API"), ];
public static List<Client> Clients { get; } = [ new Client { ClientId = "m2m.client", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = { "api1" }, }, ];}How to Write Integration Tests
Section titled “How to Write Integration Tests”WebApplicationFactory<Program> starts the IdentityServer host in memory and gives you an
HttpClient that targets it. The instance is served from localhost over HTTPS, so you can call any
endpoint directly.
Read the Discovery Document
Section titled “Read the Discovery Document”Start with the discovery document. It confirms the host boots, serves the OpenID Connect metadata, and reports no configuration errors. It is the smallest useful test and a quick way to check that the fixture itself works before you write anything more involved.
public class IdentityServerTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>{ [Fact] public async Task Discovery_document_is_available() { var client = factory.CreateClient();
var disco = await client.GetDiscoveryDocumentAsync();
Assert.False(disco.IsError); }}Request a Token
Section titled “Request a Token”Use the client from the factory to request a token with the client credentials grant, using the
m2m.client from Config:
[Fact]public async Task Can_request_client_credentials_token(){ var client = factory.CreateClient();
var response = await client.RequestClientCredentialsTokenAsync(new() { Address = "connect/token", ClientId = "m2m.client", ClientSecret = "secret", Scope = "api1", });
Assert.False(response.IsError); Assert.NotNull(response.AccessToken);}The RequestClientCredentialsTokenAsync and GetDiscoveryDocumentAsync helpers come from the
Duende.IdentityModel package.
Customize Configuration for a Single Test
Section titled “Customize Configuration for a Single Test”Because the collections in Config are mutable, a test can clear the client list and register a
client tailored to the scenario under test:
[Fact]public async Task Custom_client_can_request_token(){ Config.Clients.Clear(); Config.Clients.Add(new Client { ClientId = "test.client", ClientSecrets = { new Secret("test-secret".Sha256()) }, AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = { "api1" }, });
var client = factory.CreateClient(); var response = await client.RequestClientCredentialsTokenAsync(new() { Address = "connect/token", ClientId = "test.client", ClientSecret = "test-secret", Scope = "api1", });
Assert.False(response.IsError);}Access IdentityServer Services
Section titled “Access IdentityServer Services”Because the host runs in process, you can reach its service provider through factory.Services. This
helps when you want to assert on custom registrations or work out why a customized IdentityServer
behaves in an unexpected way:
using var scope = factory.Services.CreateScope();var profileService = scope.ServiceProvider.GetRequiredService<IProfileService>();Next Steps
Section titled “Next Steps”Integration tests validate configuration against an in-process host. To verify that a deployed instance can still complete an interactive login, see Smoke Testing a Deployed Login Flow.