使用 Active Directory 和 Windows 身份验证在 Blazor Server 中提供自定义角色

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

我正在尝试在我的 Blazor Server 应用程序中提供自定义角色。使用 Windows 身份验证进行身份验证的用户应根据其 Active Directory 组获得这些自定义角色之一,一个组代表一个角色。

如果用户在正确的组中,则用户将获得 RoleClaimType 类型的声明。这些声明稍后用于授权某些页面和操作。

我还没有看到任何人谈论 Windows 使用 Blazor 服务器的身份验证和 Active Directory,因此我有这些问题。这是我的尝试,但它是各处各部分的混合体。所以我不确定这是最好的方法还是不安全。

这是我到目前为止想出的..

ClaimTransformer.cs, 我从 appsettings.json.

得到了 Adgroup
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);
    }
}

要让 Claimstransformer 使用 Authorize 属性,请在 Startup.cs:

中使用它
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   ...

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

   ...
}
 

我还在 Startup.cs 中注册了 ClaimsTransformer: services.AddScoped<IClaimsTransformation, ClaimsTransformer>();

授权整个 Blazor 组件:

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

或授权部分组件:

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

所以我的问题基本上是:

- 这些声明是否必须重新申请?如果他们过期,他们什么时候 过期?

- 这种授权的最佳做法是什么?

- 这种方式安全吗?

你的问题有点老了,我假设你已经找到了解决方案,无论如何,也许还有其他人希望在 Windows 身份验证中实现客户角色,所以我找到的简单方法是这样的:

在服务或组件中你可以注入 AuthenticationStateProvider 然后

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

通过这种方式可以设置新的角色。

当然,您可以实现自定义逻辑,为每个用户动态添加角色。

这就是我最终添加基于 AD 组的角色的方式:

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);
        }
    }

编辑

您可以在下面找到我如何加载用户信息和分配角色的示例

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);
        }
    }

我采用了与您类似的方法,但我在作用域服务中创建了一个私有 ClaimsPrincipal 对象来存储添加的策略,因为我发现每次 TransformAsync 调用后更改都丢失了。然后我添加了一个简单的 UserInfo class 来获取经过身份验证的用户所属的所有组。

是否必须重新应用这些声明?如果过期,什么时候过期?

据我所知,每次调用 AuthenticateAsync 时都必须重新应用声明。我不确定它们是否过期,但我认为 Blazor Server 可能会 运行 TransformAsync 在向客户端发送新差异之前进行转换,因此永远不会被注意到。

此类授权的最佳做法是什么?

不知道,但只要您使用 Blazor Server,内置的身份验证和授权中间件可能是最好的方法之一。 WASM 将是一个不同的故事......

这种方式安全吗?

我认为安全问题最终会更多地集中在 Web 服务器上,而不是您分配角色的方式上。总的来说应该是比较安全的,我觉得最大的安全隐患应该是

  • 当用户从提供访问权限的组中移除时,应用程序应该立即撤销权限还是可以在下次登录时反映出来。
  • 将用户添加到无意中为他们提供访问权限的组有多容易
  • 如果权限基于 OU 等其他用户属性,如果目录发生更改,用户可能会错误地获得或失去访问权限。

用户授权服务:

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);
    }
}

用户信息:

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]);
    }
}