This will provide an example of integrating Active Directory authentication in an ASP.NET Core app.
# Active Directory Authentication
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.
## Setup
In `launchSettings.json`, you'll want to modify `iisSettings` by turning on `windowsAuthentication`:
**`launchSettings.json`**
``` 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"
}
}
}
}
```
## Identity Project
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`.
``` cs
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.
``` cs
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`.
``` cs
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).
``` cs
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`.
``` cs
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.
``` cs
using Project.Identity;
namespace Microsoft.AspNetCore.Builder
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseAdMiddleware(this IApplicationBuilder builder) =>
builder.UseMiddleware<AdUserMiddleware>();
}
}
```
## Startup Configuration
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):
``` cs
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:
``` cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Additional Configuration
app.UseAdMiddleware();
// Additional Configuration
}
```
## Accessing the Current User
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`**
``` 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;
}
```