Skip to content

Instantly share code, notes, and snippets.

@JaimeStill
Last active November 22, 2024 09:09
Show Gist options
  • Save JaimeStill/539af65518091f7b8e6b9e003a493baa to your computer and use it in GitHub Desktop.
Save JaimeStill/539af65518091f7b8e6b9e003a493baa to your computer and use it in GitHub Desktop.
ASP.NET Core Active Directory Integration

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

{
  "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.

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>();
    }
}

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):

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
}

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

[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;
}
@kbrhoades
Copy link

I am fairly new so please forgive my ignorance. Using .NET Core 3.1, most other settings are OOB. I followed the instructions above to the letter, got through any build errors, but no matter how I configure everything all i get in either Postman or via web is a 404 error. Shouldn't this http://localhost:<port#>/api/identity/GetCurrentUser work? (Going to http://localhost:<port#> brings up the typical .net intro page).

Only mods to startup file were to add
services.AddScoped<IUserProvider, AdUserProvider>(); in ConfigureServices
app.UseAdMiddleware(); in Configure

Is there something so basic going on here that I am completely missing it? Many thanks for any advice.

@MatthiasMT
Copy link

Hi All, first things first, thank you so much for the code, this is a cool tool to have!

im encountering an issue im going to try and solve on my own but incase someone has come across it... im getting this:

image

@MatthiasMT
Copy link

Hi All, first things first, thank you so much for the code, this is a cool tool to have!

im encountering an issue im going to try and solve on my own but incase someone has come across it... im getting this:

image

Fixed it.... requires some experimentation but the issue was somewhere in the AdUser.cs file. Removing several attributes solved the issue:

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment