Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 509
JohanThorild Avatar asked Mar 27 '20 10:03

JohanThorild


People also ask

How do I add authentication to Blazor server app?

Creating a Blazor application with AuthenticationWhen you hit the project type screen, select Blazor Server App then select the Change link under Authentication. From the popup window select Individual User Accounts and then OK. Make sure that Authentication is set to Individual User Accounts then click Create.

Does Kestrel support Windows authentication?

Windows Authentication (also known as Negotiate, Kerberos, or NTLM authentication) can be configured for ASP.NET Core apps hosted with IIS, Kestrel, or HTTP. sys.

How do I use Windows authentication?

On the taskbar, click Start, and then click Control Panel. In Control Panel, click Programs and Features, and then click Turn Windows Features on or off. Expand Internet Information Services, then World Wide Web Services, then Security. Select Windows Authentication, and then click OK.


2 Answers

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);
        }
    }
like image 181
Lucian Bumb Avatar answered Oct 18 '22 02:10

Lucian Bumb


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]);
    }
}
like image 2
Alex Marble Avatar answered Oct 18 '22 02:10

Alex Marble