This will provide an example of integrating Active Directory authentication in an ASP.NET Core app.
Note, you'll need to be running on a Windows domain with Visual Studio debugging in IIS Express for this to work.
In launchSettings.json
, you'll want to modify iisSettings
by turning on windowsAuthentication
:
launchSettings.json
{
"iisSettings": {
"windowsAuthentication": true,
"anonymousAuthentication": false,
"iisExpress": {
"applicationUrl": "http://localhost:5000"
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"FullstackOverview.Web": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Create a netcoreapp2.2
class library (I tend to name mine {Project}.Identity
).
You'll need to add the following NuGet packages to this library:
- Microsoft.AspNetCore.Http
- Microsoft.Extensions.Configuration.Abstractions
- Microsoft.Extensions.Configuration.Binder
- System.DirectoryServices
- System.DirectoryServices.AccountManagement
Here is the infrastructure of this class library:
- Extensions
- IdentityExtensions.cs
- MiddlewareExtensions.cs
- AdUser.cs
- AdUserMiddleware.cs
- AdUserProvider.cs
- IUserProvider.cs
AdUser.cs
I use this class so I can create a Mock implementation of this library for when I'm building outside of a domain environment. This relieves me of the dependency on UserPrincipal
.
using System;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
namespace Project.Identity
{
public class AdUser
{
public DateTime? AccountExpirationDate { get; set; }
public DateTime? AccountLockoutTime { get; set; }
public int BadLogonCount { get; set; }
public string Description { get; set; }
public string DisplayName { get; set; }
public string DistinguishedName { get; set; }
public string Domain { get; set; }
public string EmailAddress { get; set; }
public string EmployeeId { get; set; }
public bool? Enabled { get; set; }
public string GivenName { get; set; }
public Guid? Guid { get; set; }
public string HomeDirectory { get; set; }
public string HomeDrive { get; set; }
public DateTime? LastBadPasswordAttempt { get; set; }
public DateTime? LastLogon { get; set; }
public DateTime? LastPasswordSet { get; set; }
public string MiddleName { get; set; }
public string Name { get; set; }
public bool PasswordNeverExpires { get; set; }
public bool PasswordNotRequired { get; set; }
public string SamAccountName { get; set; }
public string ScriptPath { get; set; }
public SecurityIdentifier Sid { get; set; }
public string Surname { get; set; }
public bool UserCannotChangePassword { get; set; }
public string UserPrincipalName { get; set; }
public string VoiceTelephoneNumber { get; set; }
public static AdUser CastToAdUser(UserPrincipal user)
{
return new AdUser
{
AccountExpirationDate = user.AccountExpirationDate,
AccountLockoutTime = user.AccountLockoutTime,
BadLogonCount = user.BadLogonCount,
Description = user.Description,
DisplayName = user.DisplayName,
DistinguishedName = user.DistinguishedName,
EmailAddress = user.EmailAddress,
EmployeeId = user.EmployeeId,
Enabled = user.Enabled,
GivenName = user.GivenName,
Guid = user.Guid,
HomeDirectory = user.HomeDirectory,
HomeDrive = user.HomeDrive,
LastBadPasswordAttempt = user.LastBadPasswordAttempt,
LastLogon = user.LastLogon,
LastPasswordSet = user.LastPasswordSet,
MiddleName = user.MiddleName,
Name = user.Name,
PasswordNeverExpires = user.PasswordNeverExpires,
PasswordNotRequired = user.PasswordNotRequired,
SamAccountName = user.SamAccountName,
ScriptPath = user.ScriptPath,
Sid = user.Sid,
Surname = user.Surname,
UserCannotChangePassword = user.UserCannotChangePassword,
UserPrincipalName = user.UserPrincipalName,
VoiceTelephoneNumber = user.VoiceTelephoneNumber
};
}
public string GetDomainPrefix() => DistinguishedName
.Split(',')
.FirstOrDefault(x => x.ToLower().Contains("dc"))
.Split('=')
.LastOrDefault()
.ToUpper();
}
}
IUserProvider.cs
I use this interface so that I can create an additional provider in a mock library that implements this interface so I don't have to be connected to an AD domain while at home.
using System;
using System.Collections.Generic;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace Project.Identity
{
public interface IUserProvider
{
AdUser CurrentUser { get; set; }
bool Initialized { get; set; }
Task Create(HttpContext context, IConfiguration config);
Task<AdUser> GetAdUser(IIdentity identity);
Task<AdUser> GetAdUser(string samAccountName);
Task<AdUser> GetAdUser(Guid guid);
Task<List<AdUser>> GetDomainUsers();
Task<List<AdUser>> FindDomainUser(string search);
}
}
AdUserProvider.cs
Because you're using Windows authentication, the HttpContext
will contain an IIdentity
of the user logged into the domain that is accessing the web app. Because of this, we can leverage the System.DirectoryServices.AccountManagement
library to pull their UserPrincipal
.
using System;
using System.Collections.Generic;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Project.Identity.Extensions;
namespace Project.Identity
{
public class AdUserProvider : IUserProvider
{
public AdUser CurrentUser { get; set; }
public bool Initialized { get; set; }
public async Task Create(HttpContext context, IConfiguration config)
{
CurrentUser = await GetAdUser(context.User.Identity);
Initialized = true;
}
public Task<AdUser> GetAdUser(IIdentity identity)
{
return Task.Run(() =>
{
try
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
if (context != null)
{
principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, identity.Name);
}
return AdUser.CastToAdUser(principal);
}
catch (Exception ex)
{
throw new Exception("Error retrieving AD User", ex);
}
});
}
public Task<AdUser> GetAdUser(string samAccountName)
{
return Task.Run(() =>
{
try
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
if (context != null)
{
principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, samAccountName);
}
return AdUser.CastToAdUser(principal);
}
catch (Exception ex)
{
throw new Exception("Error retrieving AD User", ex);
}
});
}
public Task<AdUser> GetAdUser(Guid guid)
{
return Task.Run(() =>
{
try
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
if (context != null)
{
principal = UserPrincipal.FindByIdentity(context, IdentityType.Guid, guid.ToString());
}
return AdUser.CastToAdUser(principal);
}
catch (Exception ex)
{
throw new Exception("Error retrieving AD User", ex);
}
});
}
public Task<List<AdUser>> GetDomainUsers()
{
return Task.Run(() =>
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
principal.UserPrincipalName = "*@*";
principal.Enabled = true;
PrincipalSearcher searcher = new PrincipalSearcher(principal);
var users = searcher
.FindAll()
.AsQueryable()
.Cast<UserPrincipal>()
.FilterUsers()
.SelectAdUsers()
.OrderBy(x => x.Surname)
.ToList();
return users;
});
}
public Task<List<AdUser>> FindDomainUser(string search)
{
return Task.Run(() =>
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
principal.SamAccountName = $"*{search}*";
principal.Enabled = true;
PrincipalSearcher searcher = new PrincipalSearcher(principal);
var users = searcher
.FindAll()
.AsQueryable()
.Cast<UserPrincipal>()
.FilterUsers()
.SelectAdUsers()
.OrderBy(x => x.Surname)
.ToList();
return users;
});
}
}
}
AdUserMiddleware.cs
Custom middleware for creating the IUserProvider
instance registered with Dependency Injection (see Startup Configuration below).
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
namespace Project.Identity
{
public class AdUserMiddleware
{
private readonly RequestDelegate next;
public AdUserMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context, IUserProvider userProvider, IConfiguration config)
{
if (!(userProvider.Initialized))
{
await userProvider.Create(context, config);
}
await next(context);
}
}
}
IdentityExtensions.cs
Utility extensions for only pulling users with a Guid, and casting UserPrincipal
to AdUser
.
using System.DirectoryServices.AccountManagement;
using System.Linq;
namespace Project.Identity.Extensions
{
public static class IdentityExtensions
{
public static IQueryable<UserPrincipal> FilterUsers(this IQueryable<UserPrincipal> principals) =>
principals.Where(x => x.Guid.HasValue);
public static IQueryable<AdUser> SelectAdUsers(this IQueryable<UserPrincipal> principals) =>
principals.Select(x => AdUser.CastToAdUser(x));
}
}
MiddlewareExtensions.cs
Utility extension for making middleware registration in Startup.cs
easy.
using Project.Identity;
namespace Microsoft.AspNetCore.Builder
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseAdMiddleware(this IApplicationBuilder builder) =>
builder.UseMiddleware<AdUserMiddleware>();
}
}
To access the current user within the application, in the Startup.cs
class of your ASP.NET Core project, you need to register an IUserProvider
of type AdUserProvider
with Dependency Injection with a Scoped lifecycle (per HTTP request):
public void ConfigureServices(IServiceCollection services)
{
// Additional service registration
services.AddScoped<IUserProvider, AdUserProvider>();
// Additional service registration
}
You then need to add the AdUserMiddleware
to the middleware pipeline:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Additional Configuration
app.UseAdMiddleware();
// Additional Configuration
}
Because the IUserProvider
is configured in the middleware pipeline, and is registered with Dependency Injection, you can setup an API point to interact with the registered instance:
IdentityController.cs
[Route("api/[controller]")]
public class IdentityController : Controller
{
private IUserProvider provider;
public IdentityController(IUserProvider provider)
{
this.provider = provider;
}
[HttpGet("[action]")]
public async Task<List<AdUser>> GetDomainUsers() => await provider.GetDomainUsers();
[HttpGet("[action]/{search}")]
public async Task<List<AdUser>> FindDomainUser([FromRoute]string search) => await provider.FindDomainUser(search);
[HttpGet("[action]")]
public AdUser GetCurrentUser() => provider.CurrentUser;
}
I am facing this error
System.Exception: Error retrieving AD User
---> System.ArgumentNullException: Value cannot be null. (Parameter 'identityValue')
at System.DirectoryServices.AccountManagement.Principal.FindByIdentityWithType(PrincipalContext context, Type principalType, IdentityType identityType, String identityValue)