IdentityServer4 ships with a quickstart UI that stores clients, resources, and users in memory — ideal for getting started. When you need to move client configuration into a real database, IS4 provides a clean extension point: IClientStore. This article replaces AddInMemoryClients with a custom MongoDB store.
QuickStart Default
The quickstart registers everything from a static Config class. The InMemoryClientStore that backs IClientStore simply holds this list in a private field:
// The QuickStart startup registers test clients from a static Config class:
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryClients(Config.GetClients()) // ← static list, not from a database
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources());Replace with AddClientStore
IS4 provides AddClientStore<T> to swap in any IClientStore implementation. The store is resolved from the DI container so it can receive injected dependencies:
// Replace AddInMemoryClients with a custom store:
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddClientStore<MongoClientStore>() // ← resolved from DI, can inject IMongoDatabase
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources());IClientStore
The interface has a single method. IS4 calls it whenever it needs to validate a client or build a token:
// IClientStore — the contract IdentityServer4 calls when it needs a client
public interface IClientStore
{
Task<Client> FindClientByIdAsync(string clientId);
}Custom MongoDB Store
// Custom implementation — reads clients from MongoDB
public class MongoClientStore : IClientStore
{
private readonly IMongoCollection<Client> _collection;
public MongoClientStore(IMongoDatabase database)
=> _collection = database.GetCollection<Client>("clients");
public async Task<Client> FindClientByIdAsync(string clientId)
=> await _collection
.Find(c => c.ClientId == clientId)
.FirstOrDefaultAsync();
}Protected API
The consuming API project protects its endpoints with JwtBearer, pointing at the IS4 server as the authority:
// Protected API project — configure JWT Bearer with IS4 as authority
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5001"; // IS4 endpoint
options.Audience = "api1";
});
// Controller
[Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
[HttpGet]
public IEnumerable<WeatherForecast> Get() => _forecasts;
}Console Client
The IdentityModel package handles discovery and token exchange. Three steps: discover endpoints, exchange client credentials for a token, call the protected API:
// Console app — exchange credentials for a token then call the protected API
using IdentityModel.Client;
// Discover the IS4 endpoints
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");
// Request a client_credentials token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(
new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "client",
ClientSecret = "secret",
Scope = "api1"
});
// Call the protected API with the bearer token
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("https://localhost:6001/weatherforecast");
var content = await response.Content.ReadAsStringAsync();