简体   繁体   中英

Using Active Directory and Windows Authentication to give custom roles in Blazor Server

I'm trying to give custom roles in my Blazor Server application. User who are authenticated with Windows Authentication should be given one of these custom roles depending on their Active Directory Groups, one group represents one role.

If the user is in the correct group, then the user will be given a claim of the type RoleClaimType. These claims are later used to authorize certain pages and actions.

I haven't seen anyone talk so much about Windows Authentication and Active Directory using Blazor Server so therefore I am having these questions. This is my attempt but it is a mix of parts from here and there. So I'm not sure if this is the best way to do it or if it's unsafe.

This is what I've come up with so far..

ClaimTransformer.cs, I got the Adgroup from appsettings.json.

public class ClaimsTransformer : IClaimsTransformation
{
    private readonly IConfiguration _configuration;

    public ClaimsTransformer(IConfiguration configuration)
    {
        _configuration = configuration;
    }
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        var claimsIdentity = (ClaimsIdentity)principal.Identity
        string adGroup = _configuration.GetSection("Roles")
                    .GetSection("CustomRole")
                    .GetSection("AdGroup").Value;
        
        if (principal.IsInRole(adGroup))
        {
            Claim customRoleClaim = new Claim(claimsIdentity.RoleClaimType, "CustomRole");
            claimsIdentity.AddClaim(customRoleClaim);
        }

        return Task.FromResult(principal);
    }
}

To get the Claimstransformer to work with the Authorize attribute, use this in Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   ...

   app.UseAuthorization();
   app.UseAuthentication();

   ...
}
 

I also have registered the ClaimsTransformer in Startup.cs with: services.AddScoped<IClaimsTransformation, ClaimsTransformer>();

To authorize the whole Blazor component:

    @attribute [Authorize(Roles = "CustomRole")]

or to authorize parts of the component:

    <AuthorizeView Roles="CustomRole">
        <Authorized>You are authorized</Authorized>
    </AuthorizeView>

So my questions are basically:

- Does these claims have to be reapplied? If they expire, when do they expire?

- What is the best practice for this type of authorization?

- Is this way secure?

Your question is a bit old, I assume you already found a solution, any how, maybe there are other looking to implement custome roles in Windows Authentification, so the easies way which I found is like this:

In a service or a compenent you can inject AuthenticationStateProvider then

    var authState = await authenticationStateProvider.GetAuthenticationStateAsync();
    var user = authState.User;
    var userClaims = new ClaimsIdentity(new List<Claim>()
        {
            new Claim(ClaimTypes.Role,"Admin")
        });
    user.AddIdentity(userClaims);

In this way you can set new roles.

Of course you can implement a custom logic to add the roles dynamically for each user.

This is how I end-up adding Roles based on AD groups:

public async void GetUserAD()
        {
        var auth = await authenticationStateProvider.GetAuthenticationStateAsync();
        var user = (System.Security.Principal.WindowsPrincipal)auth.User;

        using PrincipalContext pc = new PrincipalContext(ContextType.Domain);
        UserPrincipal up = UserPrincipal.FindByIdentity(pc, user.Identity.Name);

        FirstName = up.GivenName;
        LastName = up.Surname;
        UserEmail = up.EmailAddress;
        LastLogon = up.LastLogon;
        FixPhone = up.VoiceTelephoneNumber;
        UserDisplayName = up.DisplayName;
        JobTitle = up.Description;
        DirectoryEntry directoryEntry = up.GetUnderlyingObject() as DirectoryEntry;
        Department = directoryEntry.Properties["department"]?.Value as string;
        MobilePhone = directoryEntry.Properties["mobile"]?.Value as string;
        MemberOf = directoryEntry.Properties["memberof"]?.OfType<string>()?.ToList();

        if(MemberOf.Any(x=>x.Contains("management-team") && x.Contains("OU=Distribution-Groups")))
        {
            var userClaims = new ClaimsIdentity(new List<Claim>()
            {
                new Claim(ClaimTypes.Role,"Big-Boss")
            });
            user.AddIdentity(userClaims);
        }
    }

Edit

Below you can find a sample of how I load user info and assign roles

using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.EntityFrameworkCore;
using System.DirectoryServices;
using System.DirectoryServices.AccountManagement;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;

public class UserService : IUserService
    {
        private readonly AuthenticationStateProvider authenticationStateProvider;
        private readonly ApplicationDbContext context;

        public ApplicationUser CurrentUser { get; private set; }

        public UserService(AuthenticationStateProvider authenticationStateProvider, ApplicationDbContext context)
        {
            this.authenticationStateProvider = authenticationStateProvider;
            this.context = context;
        }

        public async Task LoadCurrentUserInfoAsync()
        {
            var authState = await authenticationStateProvider.GetAuthenticationStateAsync();


            using PrincipalContext principalContext = new PrincipalContext(ContextType.Domain);
            UserPrincipal userPrincipal = UserPrincipal.FindByIdentity(principalContext, authState.User.Identity.Name);
            DirectoryEntry directoryEntry = userPrincipal.GetUnderlyingObject() as DirectoryEntry;

            CurrentUser.UserName = userPrincipal.SamAccountName;
            CurrentUser.FirstName = userPrincipal.GivenName;
            CurrentUser.LastName = userPrincipal.Surname;
            CurrentUser.Email = userPrincipal.EmailAddress;
            CurrentUser.FixPhone = userPrincipal.VoiceTelephoneNumber;
            CurrentUser.DisplayName = userPrincipal.DisplayName;
            CurrentUser.JobTitle = userPrincipal.Description;
            CurrentUser.Department = directoryEntry.Properties["department"]?.Value as string;
            CurrentUser.MobilePhone = directoryEntry.Properties["mobile"]?.Value as string;

            //get user roles from Database
            var roles = context.UserRole
                       .Include(a => a.User)
                       .Include(a => a.Role)
                       .Where(a => a.User.UserName == CurrentUser.UserName)
                       .Select(a => a.Role.Name.ToLower())
                       .ToList();

            var claimsIdentity = authState.User.Identity as ClaimsIdentity;

            //add custom roles from DataBase
            foreach (var role in roles)
            {
                var claim = new Claim(claimsIdentity.RoleClaimType, role);
                claimsIdentity.AddClaim(claim);
            }

            //add other types of claims
            var claimFullName = new Claim("fullname", CurrentUser.DisplayName);
            var claimEmail = new Claim("email", CurrentUser.Email);
            claimsIdentity.AddClaim(claimFullName);
            claimsIdentity.AddClaim(claimEmail);
        }
    }

I took a similar approach as yours but I created a private ClaimsPrincipal object in the scoped service to store the Policies that were added as I found the changes were lost after each TransformAsync Call. I then added a simple UserInfo class to get all the groups the authenticated user is a member of.

Does these claims have to be reapplied? If they expire, when do they expire?

To the best of my understanding, the claims have to be reapplied every time AuthenticateAsync is called. I'm not sure if they expire but I think Blazor Server would likely run TransformAsync prior to sending a new diff to the client so it wouldn't ever be noticed.

What is the best practice for this type of authorization?

No idea but as long as your using Blazor Server, the built in Authentication and Authorization middleware is probably one of the best approaches. WASM would be a different story though...

Is this way secure?

I think the security concerns would end up being more focused on the Web Server then on the way you assign roles. Overall it should be relatively secure, I think the biggest security concerns would be dependent on issues like

  • When a user is removed from a group that provides access, should the application immediately revoke permissions or can it be reflected on the next logon.
  • How easy can a user be added to a group that would provide them access unintentionally
  • If permissions are based on other user attributes like OU, could users gain or lose access by mistake if there are changes to the directory.

UserAuthorizationService:

public class UserAuthorizationService : IClaimsTransformation {

    public UserInfo userInfo;

    private ClaimsPrincipal CustomClaimsPrincipal;

    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal) {
        //Creates UserInfo Object on the first Call Only
        if (userInfo == null)
            userInfo = new UserInfo((principal.Identity as WindowsIdentity).Owner.Value); //Owner.Value Stores SID On Smart Card

        //Establishes CustomClaimsPrincipal on first Call
        if (CustomClaimsPrincipal == null) {
            CustomClaimsPrincipal = principal;
            var claimsIdentity = new ClaimsIdentity();

            //Loop through AD Group list and applies policies
            foreach (var group in userInfo.ADGroups) {
                switch (group) {
                    case "Example AD Group Name":
                        claimsIdentity.AddClaim(new Claim("ExampleClaim", "Test"));
                        break;
                }
            }
            CustomClaimsPrincipal.AddIdentity(claimsIdentity);
        }

        return Task.FromResult(CustomClaimsPrincipal);
    }
}

UserInfo:

public class UserInfo {

    private DirectoryEntry User { get; set; }
    public List<string> ADGroups { get; set; }

    public UserInfo(string SID) {
        ADGroups = new List<string>();
        //Retrieve Current User with SID pulled from Smart Card
        using (DirectorySearcher comps = new DirectorySearcher(new DirectoryEntry("LDAP String For AD"))) {
            comps.Filter = "(&(objectClass=user)(objectSID=" + SID + "))";
            User = comps.FindOne().GetDirectoryEntry();
        }
        //Load List with AD Group Names
        foreach (object group in User.Properties["memberOf"])
            ADGroups.Add(group.ToString()[3..].Split(",OU=")[0]);
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM