0

I have a dot net core 2.2 application that needs to have Windows Authentication with an Active Directory Group lookup to get a list of assigned groups for the current principal. These assigned groups will be the 'roles' that will be used in the Authorize attribute of certain methods. At least, in theory, that's what I'm hoping to accomplish.

I have completed the AD lookup and retrieval of the groups. At this point I'm not sure how to configure the Startup to persist this info within an auth token/cookie of some type or any UserManager/RoleManager setup kinda stuff.

Here are a couple of previous, somewhat similar questions, among others I've looked at. This previous post from .net 4.5 appears to be a similar issue, but it's the wrong version of .NET : windows-authentication-with-active-directory-groups. Can these AD groups be added as roles? Here's a potentially helpful post with this where they create a role for a user: how-to-create-roles-in-asp-net-core-2-2-and-assign-them-to-users. Confused about how this works. I've always found Identity, claims, tokens, etc. confusing so hopefully someone can assist with this in Core 2.2.

What do I need to do to get this to work? I've included most of my current code (AD code, some middleware parts, etc.), but then what? I'm sure there are others that would benefit from this too! Thank you!

I get the current Windows user and their AD record here:

return Task.Run(() =>
        {
            try
            {
                PrincipalContext context = new PrincipalContext(ContextType.Domain);
                UserPrincipal principal = new UserPrincipal(context);

                if (context != null)
                {
                    //var identityName = System.Security.Principal.WindowsIdentity.GetCurrent().Name;
                    var identityName = identity.Name;  // when windows authentication is checked
                    principal = UserPrincipal.FindByIdentity(context, IdentityType.SamAccountName, identity.Name);
                }

                return AdUser.CastToAdUser(principal);
            }
            catch (Exception ex)
            {
                //TODO LOGGING
                throw new Exception("Error retrieving AD User", ex);
            }
        });

The extension method CastToAdUser to create a more useful model is here:

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,
            Token = string.Empty,
        };
    }


return Task.Run(() =>
        {
            PrincipalSearchResult<Principal> groups = UserPrincipal.Current.GetGroups();

            IEnumerable<SecurityGroup> securityGroups = groups.Select(x => x.ToAdUserSecurityGroups());

            return securityGroups;
        });

With the extension method to create a useful model, ToAdUserSecurityGroups here:

public static SecurityGroup ToAdUserSecurityGroups (this Principal result)
    {
        var securityGroup = new SecurityGroup
        {
            Sid = result.Sid.Value,
            Name = result.SamAccountName,
            Guid = result.Guid.Value,

        };

        return securityGroup;
    }

So now I have the AD user, and the security groups that will hopefully be used for Authorization. I wire in my AD lookup stuff using some custom middleware, called UseAdMiddleWare. In my Startup class, I have an extension in the Configure method to fire off all the above 'stuff':

app.UseAdMiddleware();

And in my ConfigureServices I have the AddAuthentication stuff, which is needed, but might not be configured correctly for what I'm trying to do:

services.AddAuthentication();

In separate classes I have the code that allows this. The IAdUserProvider is my own class that does the AD lookup, with an entry point called Create:

 public static class MiddlewareExtensions
{
      public static IApplicationBuilder UseAdMiddleware(this IApplicationBuilder builder) =>
        builder.UseMiddleware<AdUserMiddleware>();
}

public class AdUserMiddleware
{
    private readonly RequestDelegate next;

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

    public async Task Invoke(HttpContext context, IAdUserProvider userProvider, IConfiguration config)
    {
        if (!(userProvider.Initialized))
        {
            await userProvider.Create(context, config);
        }

        await next(context);
    }
}

So I think I'm well on my way to getting this wired up, but how/where do I add the security group specifics into claims or whatever? Thank you very much!

Nan Yu
  • 26,101
  • 9
  • 68
  • 148
RichieMN
  • 905
  • 1
  • 12
  • 33
  • 1
    windows authentication is easy to implement in asp.net core web application . and you can use custom IClaimsTransformer interface to add group as roles . Is that your scenario ? – Nan Yu Sep 04 '19 at 07:21
  • Thanks for the reply! Yes, this is what I did, and I'll include code below shortly. – RichieMN Sep 04 '19 at 14:43

1 Answers1

0

I (mostly) found a solution to this using Nan's recommendation to use the IClaimsTransformer. The concrete implementation of this class fires every Authorize request, and I'm not sure if there's a possible way to persist these claims?

Here's my Startup.ConfigureServices, where I have some IIS options to automatically log in using my Windows auth, and there's the line to create the singleton of my IClaimsTransformation:

public void ConfigureServices(IServiceCollection services)
    {
        services.AddCors();

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);            

        services.Configure<IISServerOptions>(options =>
        {
            options.AutomaticAuthentication = true;
        });

        services.Configure<IISOptions>(options =>
        {
            options.AutomaticAuthentication = true;
            options.ForwardClientCertificate = true;
        });

        services.AddSingleton<IClaimsTransformation, CustomClaimsTransformation>();

        services.AddAuthentication(IISDefaults.AuthenticationScheme);
   }

In Startup.Configure I have this: Do I need the cookiepolicy?

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {

        // Add whatever you typically need here...

        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseCookiePolicy();          

        app.UseAuthentication();            

        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });
    }

My CustomClaimsTransformation is here, and this fires at each Authorize. Is this normal? I'm adding the Security Groups as ROLES so I can use these to Authorize the users based on the groups they are assigned. I had hoped that this would be handled once, and the claims would be permanent for the duration. Thoughts on this?

public class CustomClaimsTransformation : IClaimsTransformation
{
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        //add new claim

        // Check and see if Groups are already part of the principal, and add them as claims.
        // var groups = userClaimsId.Claims.Where(x => x.Type.Equals("groups")).ToList();


        var ci = (ClaimsIdentity)principal.Identity;
            var c = new Claim(ci.RoleClaimType, "Super_Special_User");
            ci.AddClaim(c);
            return Task.FromResult(principal);
    }
}

Within the Controller I add the Authorize attribute (seems to be case sensitive). It might be a good ideas to create a static class of role string constants to hold all these values. Keeps you free of the magic strings all over the place.

[Authorize(Roles = "Super_Special_User")]

Please let me know if I can improve this! Thanks for your time!

RichieMN
  • 905
  • 1
  • 12
  • 33