Skip to content

Instantly share code, notes, and snippets.

@JaimeStill
Last active July 9, 2024 04:38
Show Gist options
  • Save JaimeStill/58e68ab82a394ae69928e2e59c99d4cc to your computer and use it in GitHub Desktop.
Save JaimeStill/58e68ab82a394ae69928e2e59c99d4cc to your computer and use it in GitHub Desktop.
ASP.NET Core Active Directory Identity Integration Middleware

Caveats

Since this implementation relies on the System.DirectoryServices and System.DirectoryServices.AccountManagement libraries, the ASP.NET Core project must target .NET Framework rather than .NET Core or .NET Standard.

Add references to System.DirectoryServices and System.DirectoryServices.AccountManagement in the ASP.NET Core project

<PropertyGroup>
  <TargetFramework>net471</TargetFramework>
  <RuntimeIdentifiers>win10-x64</RuntimeIdentifiers>
  <TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
  <TypeScriptToolsVersion>Latest</TypeScriptToolsVersion>
  <IsPackable>false</IsPackable>
</PropertyGroup>

Also, Windows Authentication must be used:

launchSettings.json

{
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymouseAuthentication": false
  }
}
// User entity class added for convenience. All other entities removed for clarity
namespace App.Data
{
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<User> Users { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var rel in modelBuilder.Model.GetEntityTypes().SelectMany(x => x.GetForeignKeys()))
{
rel.DeleteBehavior = DeleteBehavior.Restrict;
}
foreach (var ent in modelBuilder.Model.GetEntityTypes().Select(x => x.Name))
{
var name = ent.Split('.').Last();
modelBuilder.Entity(ent).ToTable(name);
}
}
}
public class User
{
public int Id { get; set; }
public Guid Guid { get; set; }
public string DisplayName { get; set; }
public string UserPrincipal { get; set; }
public string Theme { get; set; }
public string Sidepanel { get; set; }
public bool IsAdmin { get; set; }
public bool IsDeleted { get; set; }
}
}
namespace App.Web.Extensions
{
public static class DirectoryExtensions
{
public static Task<UserPrincipal> GetUserPrincipal(this IIdentity identity)
{
return Task.Run(() =>
{
try
{
WindowsIdentity windowsIdentity = identity as WindowsIdentity;
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
if (context != null)
{
principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, windowsIdentity.Name);
}
return principal;
}
catch (Exception ex)
{
throw new Exception(ex.GetExceptionMessageChain());
}
});
}
public static Task<UserPrincipal> GetUserPrincipal(this 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 principal;
}
catch (Exception ex)
{
throw new Exception(ex.GetExceptionMessageChain());
}
});
}
public static Task<List<UserPrincipalModel>> FindDomainUser(this string username)
{
return Task.Run(() =>
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
principal.UserPrincipalName = $"*{username}*";
principal.DisplayName = "*,*";
principal.Enabled = true;
PrincipalSearcher searcher = new PrincipalSearcher(principal);
var users = searcher.FindAll()
.AsQueryable()
.Cast<UserPrincipal>()
.FilterUsers()
.SelectUsers()
.OrderBy(x => x.displayName)
.ToList();
return users;
});
}
public static Task<List<UserPrincipalModel>> GetDomainUsers()
{
return Task.Run(() =>
{
PrincipalContext context = new PrincipalContext(ContextType.Domain);
UserPrincipal principal = new UserPrincipal(context);
principal.UserPrincipalName = "*@*";
principal.DisplayName = "*,*";
principal.Enabled = true;
PrincipalSearcher searcher = new PrincipalSearcher(principal);
var users = searcher.FindAll()
.AsQueryable()
.Cast<UserPrincipal>()
.FilterUsers()
.SelectUsers()
.ToList();
return users;
});
}
public static IQueryable<UserPrincipal> FilterUsers(this IQueryable<UserPrincipal> users)
{
return users.Where(x => x.Guid.HasValue // and / or any additional filters);
}
public static IQueryable<UserPrincipalModel> SelectUsers(this IQueryable<UserPrincipal> users)
{
return users.Select(x => new UserPrincipalModel
{
guid = x.Guid.Value,
displayName = x.DisplayName,
samAccountName = x.SamAccountName,
userPrincipalName = x.UserPrincipalName,
distinguishedName = x.DistinguishedName
});
}
}
}
namespace App.Web.Extensions
{
public static class IdentityExtensions
{
public static IApplicationBuilder UseUserMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<UserMiddleware>();
}
public static async Task<User> AddOrUpdate(this UserPrincipal principal, AppDbContext db)
{
var user = await db.Users.FirstOrDefaultAsync(x => x.Guid == principal.Guid.Value);
if (user == null)
{
user = new User();
await user.CreateUser(principal, db);
}
else
{
user = await user.UpdateUser(principal, db);
}
return user;
}
public static async Task<User> CreateUser(this User user, UserPrincipal principal, AppDbContext db)
{
user.DisplayName = await principal.SetDisplayName();
user.UserPrincipal = principal.UserPrincipalName;
user.Guid = principal.Guid.Value;
user.Theme = "green-light";
user.Sidepanel = "full";
user.IsAdmin = false;
user.IsDeleted = false;
await db.Users.AddAsync(user);
await db.SaveChangesAsync();
return user;
}
public static async Task<User> UpdateUser(this User user, UserPrincipal principal, AppDbContext db)
{
user.DisplayName = awiat principal.SetDisplayName();
user.UserPrincipal = principal.UserPrincipalName;
user.Guid = principal.Guid.Value;
await db.SaveChangesAsync();
return user;
}
public static async Task<string> SetDisplayName(this UserPrincipal principal)
{
var rank = await principal.GetProperty("title");
var splitName = principal.SamAccountName.Split('.');
string displayName;
if (splitName.Count() > 1)
{
displayName = ($"{rank} {splitName[1]}, {splitName[0]}").Trim();
}
else
{
displayName = ($"{rank} {principal.DisplayName}").Trim();
}
return displayName;
}
public static Task<string> GetProperty(this UserPrincipal principal, string property)
{
return Task.Run(() =>
{
try
{
DirectoryEntry entry = principal.GetUnderlyingObject() as DirectoryEntry;
if (entry.Properties.Contains(property))
{
return entry.Properties[property].Value.ToString();
}
return string.Empty;
}
catch
{
return string.Empty;
}
});
}
}
}
namespace App.Web
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// Entity Framework Core Integration - https://github.com/JaimeStill/EFCoreIntegration/wiki
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
services.AddDbContext<AppDbContext>(optiosn => options.UseSqlServer(Configuration.GetConnectionString("Default")));
services.AddScoped<UserManager>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// Extraneous configuration removed
app.UseUserMiddleware();
}
}
}
namespace App.Web.Extensions
{
public static class UserExtensions
{
public static IQueryable<User> SetUserIncludes(this DbSet<User> users)
{
return users.Include(x => x.NavigationProperty)
.Include(x => // chain together .Include() with need navigation properties);
}
public static async Task<List<User>> GetUsers(this AppDbContext db)
{
var users = await db.Users.SetUserIncludes().Where(x => !x.IsDeleted).ToListAsync();
return users;
}
public static async Task<List<User>> GetDeletedUsers(this AppDbContext db)
{
var users = await db.Users.SetUserIncludes().Where(x => x.IsDeleted).ToListAsync();
return users;
}
public static async Task<List<User>> GetAdminUsers(this AppDbContext db)
{
var users = await db.Users.SetUserIncludes().Where(x => !x.IsDeleted && x.IsAdmin).ToListAsync();
return users;
}
public static async Task<User> GetUser(this AppDbContext db, int id)
{
var user = await db.Users.SetUserIncludes().FirstOrDefaultAsync(x => x.Id == id);
return user;
}
public static async Task AddUser(this AppDbContext db, string guid)
{
var parse = Guid.Parse(guid);
if (await parse.Validate(db))
{
var principal = await parse.GetUserPrincipal();
var user = new User();
await user.CreateUser(principal, db);
}
else
{
throw new Exception("The selected user already has an account");
}
}
public static async Task UpdateUser(this AppDbContext db, User user)
{
db.Entry(user).State = EntityState.Modified;
await db.SaveChangesAsync();
}
public static async Task ToggleIsAdmin(this AppDbContext db, int id)
{
var user = await db.Users.FindAsync(id);
user.IsAdmin = !user.IsAdmin;
await db.SaveChangesAsync();
}
public static async Task ToggleUserDeleted(this AppDbContext db, int id)
{
var user = await db.Users.FindAsync(id);
user.IsDeleted = !user.IsDeleted;
await db.SaveChangesAsync();
}
public static async Task<bool> Validate(this Guid guid, AppDbContext db)
{
var user = await db.Users.FirstOrDefaultAsync(x => x.Guid == guid);
return user == null;
}
}
}
namespace App.Web.Infrastructure
{
public class UserManager
{
private AppDbContext db;
public User CurrentUser { get; set; }
public bool Initialized { get; set; }
public UserManager(AppDbContext db)
{
this.db = db;
}
public async Task Create(HttpContext context)
{
var userPrincipal = await context.User.Identity.GetUserPrincipal();
// Keep current user in sync with ActiveDirectory
CurrentUser = await userPrincipal.AddOrUpdate(db);
Initialized = true;
}
public void Dispose()
{
CurrentUser = new User();
Initialized = false;
}
}
}
namespace App.Web.Infrastructure
{
private readonly RequestDelegate _next;
public UserMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext, httpContext, UserManager userManager)
{
if (!userManager.Initialized)
{
await userManager.Create(httpContext);
}
await _next(httpContext);
userManager.Dispose();
}
}
namespace App.Web.Models
{
public class UserPrincipalModel
{
public Guid guid { get; set; }
public string samAccountname { get; set; }
public string userPrincipalName { get; set; }
public string displayName { get; set; }
public string distinguishedName { get; set; }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment