Skip to content

Instantly share code, notes, and snippets.

@oone-dev
Forked from JaimeStill/README.md
Created September 16, 2024 09:18
Show Gist options
  • Save oone-dev/acfa6f74e7444f4c9347fea9be699112 to your computer and use it in GitHub Desktop.
Save oone-dev/acfa6f74e7444f4c9347fea9be699112 to your computer and use it in GitHub Desktop.

Revisions

  1. @JaimeStill JaimeStill revised this gist Jun 3, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -410,7 +410,7 @@ You then need to add the `AdUserMiddleware` to the middleware pipeline:
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
    // Additional Configuration
    app.UseAdMiddleare();
    app.UseAdMiddleware();
    // Additional Configuration
    }
    ```
  2. @JaimeStill JaimeStill revised this gist Mar 6, 2019. 1 changed file with 0 additions and 4 deletions.
    4 changes: 0 additions & 4 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -276,11 +276,9 @@ namespace Project.Identity
    return Task.Run(() =>
    {
    PrincipalContext context = new PrincipalContext(ContextType.Domain);
    GroupPrincipal group = new GroupPrincipal(context, AppGroup);
    UserPrincipal principal = new UserPrincipal(context);
    principal.UserPrincipalName = "*@*";
    principal.Enabled = true;
    principal.IsMemberOf(group);
    PrincipalSearcher searcher = new PrincipalSearcher(principal);

    var users = searcher
    @@ -301,11 +299,9 @@ namespace Project.Identity
    return Task.Run(() =>
    {
    PrincipalContext context = new PrincipalContext(ContextType.Domain);
    GroupPrincipal group = new GroupPrincipal(context, AppGroup);
    UserPrincipal principal = new UserPrincipal(context);
    principal.SamAccountName = $"*{search}*";
    principal.Enabled = true;
    principal.IsMemberOf(group);
    PrincipalSearcher searcher = new PrincipalSearcher(principal);

    var users = searcher
  3. @JaimeStill JaimeStill revised this gist Feb 23, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -2,7 +2,7 @@

    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 debuggin in IIS Express for this to work.
    > Note, you'll need to be running on a Windows domain with Visual Studio debugging in IIS Express for this to work.
    ## Setup

  4. @JaimeStill JaimeStill revised this gist Feb 23, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -2,7 +2,7 @@

    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 debugggin in IIS Express for this to work.
    > Note, you'll need to be running on a Windows domain with Visual Studio debuggin in IIS Express for this to work.
    ## Setup

  5. @JaimeStill JaimeStill created this gist Feb 22, 2019.
    447 changes: 447 additions & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,447 @@
    # 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 debugggin 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);
    GroupPrincipal group = new GroupPrincipal(context, AppGroup);
    UserPrincipal principal = new UserPrincipal(context);
    principal.UserPrincipalName = "*@*";
    principal.Enabled = true;
    principal.IsMemberOf(group);
    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);
    GroupPrincipal group = new GroupPrincipal(context, AppGroup);
    UserPrincipal principal = new UserPrincipal(context);
    principal.SamAccountName = $"*{search}*";
    principal.Enabled = true;
    principal.IsMemberOf(group);
    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.UseAdMiddleare();
    // 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;
    }
    ```