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?
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.
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.
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.
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
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]);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With