I had to design a solution to this problem this way:
1. Any AD authenticated user will be able to access the application.
2. The roles and claims of the users are stored in the Identity database of the application.
3. An admin user will be able to assign roles to users (I have added this functionality to the app as well).
Read on if you want to see my complete solution. The link to the full source code is towards the end of this answer.
Basic design
1. User enters Active Directory credentials (Windows login credentials in this case).
2. The app checks if it's a valid login against AD.
2.1. If it's not valid, app returns the page with 'Invalid login attempt' error message.
2.2. If it's valid, go to next step.
3. Check if the user exists in the Identity database.
3.1. If Not, create this user in our Identity database.
3.2 If Yes, go to next step.
4. SignIn the user (using AD credentials). This is where we override UserManager.
Note: The user created in step 3.1 has no roles assigned.
An admin user (with valid AD username) is created during Db initialization. Adjust the Admin2UserName
with your AD username if you want to be the admin user who will assign roles to newly added users. Don't even worry about the password, it can be anything because the actual authentication will happen through AD not through what's in Identity database.
Solution
Step 1:
Ensure that you've got Identity setup in your application. As an example, I'm taking a Blazor Server app here. If you don't have Identity setup, follow this guide from Microsoft learn.
Use my project to follow along the guide.
Step 2:
Add ADHelper
static class to help with Active Directory login. In my example, it's at Areas/Identity/ADHelper.cs and has contents that look like this:
using System.DirectoryServices.AccountManagement;
namespace HMT.Web.Server.Areas.Identity
{
public static class ADHelper
{
public static bool ADLogin(string userName, string password)
{
using PrincipalContext principalContext = new(ContextType.Domain);
bool isValidLogin = principalContext.ValidateCredentials(userName.ToUpper(), password);
return isValidLogin;
}
}
}
Step 3:
Override CheckPasswordAsync
method in UserManager
so you can authenticate users against Active Directory. I have done this in Areas/Identity/ADUserManager.cs, the contents of which look like this:
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
namespace HMT.Web.Server.Areas.Identity
{
public class ADUserManager<TUser> : UserManager<TUser> where TUser : IdentityUser
{
public ADUserManager(IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor,
IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidators,
IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<TUser>> logger)
: base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer,
errors, services, logger)
{
}
public override Task<bool> CheckPasswordAsync(TUser user, string password)
{
var adLoginResult = ADHelper.ADLogin(user.UserName, password);
return Task.FromResult(adLoginResult);
}
}
}
Step 4:
Register it in your Program.cs
builder.Services
.AddDefaultIdentity<ApplicationUser>(options =>
{
options.SignIn.RequireConfirmedAccount = false;
})
.AddRoles<ApplicationRole>()
.AddUserManager<CustomUserManager<ApplicationUser>>() <----- THIS GUY
.AddEntityFrameworkStores<ApplicationDbContext>();
ApplicationUser
, ApplicationRole
and ApplicationDbContext
look like this:
public class ApplicationUser : IdentityUser
{
}
public class ApplicationRole : IdentityRole
{
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
}
}
Step 5:
Update OnPostAsync
method in Areas/Identity/Pages/Account/Login.cshtml.cs to implement the authentication flow. The method looks like this:
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl ??= Url.Content("~/");
if (ModelState.IsValid)
{
// Step 1: Authenticate an user against AD
// If YES: Go to next step
// If NO: Terminate the process
var adLoginResult = ADHelper.ADLogin(Input.UserName, Input.Password);
if (!adLoginResult)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
// Step 2: Check if the user exists in our Identity Db
// If YES: Proceed to SignIn the user
// If NO: Either terminate the process OR create this user in our Identity Db and THEN proceed to SignIn the user
// I'm going with OR scenario this time
var user = await _userManager.FindByNameAsync(Input.UserName);
if (user == null)
{
var identityResult = await _userManager.CreateAsync(new ApplicationUser
{
UserName = Input.UserName,
}, Input.Password);
if (identityResult != IdentityResult.Success)
{
ModelState.AddModelError(string.Empty, "The user was authenticated against AD successfully, but failed to be inserted into Application's Identity database.");
foreach (IdentityError error in identityResult.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
return Page();
}
}
// Step 3: SignIn the user using AD credentials
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
_logger.LogInformation("User logged in.");
return LocalRedirect(returnUrl);
}
if (result.RequiresTwoFactor)
{
return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
}
if (result.IsLockedOut)
{
_logger.LogWarning("User account locked out.");
return RedirectToPage("./Lockout");
}
else
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
}
// If we got this far, something failed, redisplay form
return Page();
}
Basically, we're done here.
Step 6:
Now if an admin user wants to assign roles to newly added users, simply go to Manage Users page and assign appropriate roles.
Pretty easy, right?

Step 7:
If you want to manage roles (add, edit, delete), simply go to manage/roles page.

Conclusion
This setup ensures that users are authenticated using Active Directory and are authorized using roles in the Identity database.
Complete source code
https://github.com/affableashish/blazor-server-auth/tree/feature/AddADAuthentication