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

danielts86 commented Nov 26, 2019

@sgsNone - Can you share your AnonMiddleware class and usage

@sgsnikola1
Copy link

@danielts86, here you go.
class

   public class AnonymousMiddleware
    {
        private readonly RequestDelegate next;

        public AnonymousMiddleware(RequestDelegate next)
        {
            this.next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            if (context.User.Identity.IsAuthenticated)
            {
                await next(context);
                return;
            }

            await context.ChallengeAsync("Windows");
        }
    }

extension

public static IApplicationBuilder UseAnonMiddleware(this IApplicationBuilder builder) =>
            builder.UseMiddleware<AnonymousMiddleware>();

startup

app.UseCors("CorsPolicy");
app.UseAnonMiddleware();
app.UseAdMiddleware();
app.UseMvc();

@Vrankela
Copy link

Vrankela commented Feb 7, 2020

I am struggling with getting this working. I am getting an error. See below. Is there something i need to pass in to get the current app user?

_An unhandled exception occurred while processing the request.
ArgumentNullException: Value cannot be null.
Parameter name: identityValue
System.DirectoryServices.AccountManagement.Principal.FindByIdentityWithType(PrincipalContext context, Type principalType, IdentityType identityType, string identityValue)

Exception: Error retrieving AD User
CoreIdentity.AdUserProvider+<>c__DisplayClass9_0.b__0() in AdUserProvider.cs, line 42_

I have the exact same problem but only when I deploy the project to IIS server, locally however this works just fine. Has anyone had this issue and solved it?

@JaimeStill
Copy link
Author

@Vrankela, did you ensure that Windows Authentication is enabled and Anonymous Authentication is disabled on your IIS server? And that you are accessing the deployed app from a domain account? The error you're showing indicates that the user identity is not being provided to your app.

@Vrankela
Copy link

Vrankela commented Feb 7, 2020

@Vrankela, did you ensure that Windows Authentication is enabled and Anonymous Authentication is disabled on your IIS server? And that you are accessing the deployed app from a domain account? The error you're showing indicates that the user identity is not being provided to your app.

Yes Jamie, all of that. I even tested how it behaves when I just leave the windows authentication in launchSettings.json and remove the dependency injection from the two methods in Startup.cs. Then the browser just prompts me for my domain password and my app runs normally. But I really need your implementation as it allows me to further develop on it and filter through authorized users and user groups...

@hattabatatta
Copy link

hattabatatta commented Mar 18, 2020

Sorry for a maybe stupid question but how to implement a "filter" for user groups, based on MVC Controllers?
Like "SuperAdminController" is only accessible by the AD group "SuperAdmins" and a "NormalADUserController" which can be accessed by every user from the AD?
Like an authentication within an authentication ... authentiception :D

I hope you know what I mean :)

Every controller (also API controllers) will be first check if the call comes from an AD user, which is fine, but I need also want to have "user rights"

@JaimeStill
Copy link
Author

@hattabatatta perhaps this StackOverflow answer would be helpful?

@hattabatatta
Copy link

@hattabatatta perhaps this StackOverflow answer would be helpful?

Many thx for your quick answer
I´ll take a look at it ... I thought on dependency injection into contollers in first hand but if this will work ... also welcomed :)

@travbeamo
Copy link

Excellent work.

Do you know if/how this could possibly be converted into an external provider authentication in ASP.NET Core, where Startup.cs for the project would include something like:

services.AddAuthentication().AddActiveDirectory(options => {
  options.DomainName = Configuration["Authentication:ActiveDirectory:DomainName"];
});

@JaimeStill
Copy link
Author

Excellent work.

Do you know if/how this could possibly be converted into an external provider authentication in ASP.NET Core, where Startup.cs for the project would include something like:

services.AddAuthentication().AddActiveDirectory(options => {
  options.DomainName = Configuration["Authentication:ActiveDirectory:DomainName"];
});

@travbeamo I can't imagine this being too difficult to implement. That said, my bandwidth is very limited at the moment. If you're able to figure out an implementation for this, it would be awesome if you could share.

@MostafaNouri
Copy link

I'm using the IIS Express to deploy your nice project. When I use my local user (not administrator) the browser pop-up login form shows up but I want my custom login page. is it possible or not? I know that my question is somehow irrelevant to this context but I really need to know how can I do that.

@ajeyaprakash148
Copy link

I am getting below error.

System.DirectoryServices.AccountManagement.PrincipalServerDownException: 'The server could not be contacted.'

is there any help? whether we need to pass domain, user, pwd details?
PrincipalContext context = new PrincipalContext(ContextType.Domain);

@willerpp
Copy link

Excellent code, it works like a charm!!! thank you

@chantholkhom
Copy link

I am facing this error
2021-01-26_11-50-51

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)

@krizskp
Copy link

krizskp commented Mar 1, 2022

Anyone found a solution to this?

@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