Converting IdentityServer4 from In-Memory Stores to a Database-Backed User Service

This guide demonstrates how to replace the default in-memory user store in IdentityServer4 with a custom service that queries a database. While IdentityServer4 provides Entity Framework (EF) based data sources, this approach shows how to implement a database-driven user store by extending its core interfaces. The focus will be on the user authentication logic, while client and API configurations remain static.

Prerequisites: Start with a new IdentityServer4 project using the in-memory template.

dotnet new is4inmem

This command creates a project with in-memory stores and test users, providing a solid foundation for our modifications.

Step 1: Identify the Default User Store

The default user authentication logic is encapsulated within the TestUserStore class. Our goal is to replace this with our own implementation that interacts with a database.

Step 2: Create a Custom User Service

First, define an interface for our user service. This interface will mirror the methods rqeuired by IdentityServer4's user store contract.

public interface IUserRepository
{
   Task<MyUser> FindBySubjectIdAsync(string subjectId);
   Task<MyUser> FindByUsernameAsync(string username);
   Task<bool> ValidateCredentialsAsync(string username, string password);
}

Next, implement this interface. You can begin by copying the logic from the original TestUserStore and then modify it to perform database queries instead of in-memory lookups. The key methods to implement are FindBySubjectIdAsync, FindByUsernameAsync, and ValidateCredentialsAsync.

public class DatabaseUserRepository : IUserRepository
{
   private readonly MyDbContext _dbContext;

   public DatabaseUserRepository(MyDbContext dbContext)
   {
       _dbContext = dbContext;
   }

   public async Task<MyUser> FindBySubjectIdAsync(string subjectId)
   {
       // Implement database query to find a user by their unique Subject ID
       return await _dbContext.Users.FirstOrDefaultAsync(u => u.SubjectId == subjectId);
   }

   public async Task<MyUser> FindByUsernameAsync(string username)
   {
       // Implement database query to find a user by their username
       return await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username);
   }

   public async Task<bool> ValidateCredentialsAsync(string username, string password)
   {
       // Implement logic to validate username and password against the database
       var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Username == username);
       if (user == null) return false;
       return user.Password == password; // Note: Use proper password hashing in a real application
   }
}

Step 3: Register the Service in Dependency Injection

Register the new user repository with the dependency injection container in the Startup.cs file within the ConfigureServices method.

public void ConfigureServices(IServiceCollection services)
{
   // ... other service registrations

   services.AddIdentityServer()
       .AddInMemoryClients(Config.Clients)
       .AddInMemoryApiResources(Config.ApiResources)
       .AddInMemoryApiScopes(Config.ApiScopes)
       .AddTestUsers(Config.TestUsers); // Keep this for testing if needed

   // Register our custom database user repository
   services.AddTransient<IUserRepository, DatabaseUserRepository>();

   // Register custom services for profile and password validation
   services.AddTransient<IProfileService, CustomProfileService>();
   services.AddTransient<IResourceOwnerPasswordValidator, CustomPasswordValidator>();
}

Step 4: Implement Profile and Password Validation Services

IdentityServer4 uses specific services to handle user profile data and password-based authentication. We must provide our own implementations.

// Custom Profile Service
public class CustomProfileService : IProfileService
{
   private readonly IUserRepository _userRepository;

   public CustomProfileService(IUserRepository userRepository)
   {
       _userRepository = userRepository;
   }

   public async Task GetProfileDataAsync(ProfileDataRequestContext context)
   {
       // Retrieve user claims from the database and set them in the context
       var user = await _userRepository.FindBySubjectIdAsync(context.Subject.GetSubjectId());
       if (user != null)
       {
           var claims = new List<Claim>
           {
               new Claim("name", user.Username),
               // Add other claims as needed
           };
           context.IssuedClaims = claims;
       }
   }

   public async Task IsActiveAsync(IsActiveContext context)
   {
       // Determine if the user is active
       var user = await _userRepository.FindBySubjectIdAsync(context.Subject.GetSubjectId());
       context.IsActive = user != null;
   }
}

// Custom Password Validator
public class CustomPasswordValidator : IResourceOwnerPasswordValidator
{
   private readonly IUserRepository _userRepository;

   public CustomPasswordValidator(IUserRepository userRepository)
   {
       _userRepository = userRepository;
   }

   public async Task<TokenValidationResult> ValidateAsync(ResourceOwnerPasswordValidationContext context)
   {
       var user = await _userRepository.FindByUsernameAsync(context.UserName);
       if (user != null && await _userRepository.ValidateCredentialsAsync(context.UserName, context.Password))
       {
           context.Result = new TokenValidationResult
           {
               IsError = false,
               UserInfo = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
               {
                   new Claim("sub", user.SubjectId),
                   new Claim("name", user.Username)
               }))
           };
       }
       else
       {
           context.Result = new TokenValidationResult
           {
               IsError = true,
               Error = "invalid_grant"
           };
       }
       return context.Result;
   }
}

Step 5: Remove the In-Memory User Store

Finally, ensure the original in-memory user store is not registered, as it would conflict with our new database service. The line that registers the TestUserStore should be removed or commented out.

// This line should be removed or commented out
// services.AddSingleton<IUserStore<TestUser>>(new TestUserStore(Config.TestUsers));

With these changes, IdentityServer4 will now use your database to auhtenticate users, validate credentials, and retrieve user profile information.

Tags: IdentityServer4 ASP.NET Core Authentication database User Store

Posted on Thu, 03 Sep 2026 16:33:26 +0000 by inferium