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

Joh*_*ild 5 c# authorization windows-authentication asp.net-core blazor

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

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

我还没有看到任何人对使用 Blazor Server 的 Windows 身份验证和 Active Directory 进行过多讨论,因此我遇到了这些问题。这是我的尝试,但它混合了这里和那里的部分。所以我不确定这是最好的方法还是不安全。

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

ClaimTransformer.cs,我从 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

   ...
}
 
Run Code Online (Sandbox Code Playgroud)

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

授权整个 Blazor 组件:

    @attribute [Authorize(Roles = "CustomRole")]
Run Code Online (Sandbox Code Playgroud)

或授权部分组件:

    <AuthorizeView Roles="CustomRole">
        <Authorized>You are authorized</Authorized>
    </AuthorizeView>
Run Code Online (Sandbox Code Playgroud)

所以我的问题基本上是:

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

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

- 这种方式安全吗?

Luc*_*umb 6

你的问题有点老了,我假设你已经找到了一个解决方案,无论如何,也许还有其他人希望在 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);
Run Code Online (Sandbox Code Playgroud)

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

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

这就是我最终根据 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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

编辑

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

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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

  • @JMooney我有一个简单的类“UserInfo”,我在构造函数中注入“AuthenticationStateProvider”。我还在启动“ConfigureServices”中将“UserInfo”注册为“Scoped” (2认同)

小智 2

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

这些主张是否必须重新适用?如果过期,什么时候过期?

据我所知,每次调用 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

用户信息:

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]);
    }
}
Run Code Online (Sandbox Code Playgroud)