ASP.NET Core Custom Role/Policy Authorization with JWT: Required Interfaces

2019年12月15日 2489点热度 0人点赞 0条评论
内容目录

[TOC]

① Store API Accessible by Roles/Users

For example,

Use List<ApiPermission> to store the authorized API list for the role.

Optional.

Authorized APIs can also be stored in the Token, which can solely contain role information and user identity information.

    /// &lt;summary&gt;
    /// API
    /// &lt;/summary&gt;
    public class ApiPermission
    {
        /// &lt;summary&gt;
        /// API Name
        /// &lt;/summary&gt;
        public virtual string Name { get; set; }
        /// &lt;summary&gt;
        /// API URL
        /// &lt;/summary&gt;
        public virtual string Url { get; set; }
    }

② Implement IAuthorizationRequirement Interface

The IAuthorizationRequirement interface represents the user's identity information and is used for authentication verification and authorization verification.

In fact, IAuthorizationRequirement does not have any content to implement.

namespace Microsoft.AspNetCore.Authorization
{
    //
    // Summary:
    //     Represents an authorization requirement.
    public interface IAuthorizationRequirement
    {
    }
}

By implementing IAuthorizationRequirement, you can define any needed properties, which will serve as a convenience for custom validation. To see how to use it, you can define it as a global identifier and set globally applicable data. I later found that my implementation was not very good:

    // IAuthorizationRequirement is the Microsoft.AspNetCore.Authorization interface

    /// &lt;summary&gt;
    /// Necessary parameters class for user authentication information
    /// &lt;/summary&gt;
    public class PermissionRequirement : IAuthorizationRequirement
    {
        /// &lt;summary&gt;
        /// User's associated role
        /// &lt;/summary&gt;
        public Role Roles { get; set; } = new Role();
        public void SetRolesName(string roleName)
        {
            Roles.Name = roleName;
        }
        /// &lt;summary&gt;
        /// Redirect to this API when there is no permission
        /// &lt;/summary&gt;
        public string DeniedAction { get; set; }

        /// &lt;summary&gt;
        /// Authentication and authorization type
        /// &lt;/summary&gt;
        public string ClaimType { internal get; set; }
        /// &lt;summary&gt;
        /// Redirect when unauthorized
        /// &lt;/summary&gt;
        public string LoginPath { get; set; } = &quot;/Account/Login&quot;;
        /// &lt;summary&gt;
        /// Issuer
        /// &lt;/summary&gt;
        public string Issuer { get; set; }
        /// &lt;summary&gt;
        /// Audience
        /// &lt;/summary&gt;
        public string Audience { get; set; }
        /// &lt;summary&gt;
        /// Expiration time
        /// &lt;/summary&gt;
        public TimeSpan Expiration { get; set; }
        /// &lt;summary&gt;
        /// Issued time
        /// &lt;/summary&gt;
        public long IssuedTime { get; set; }
        /// &lt;summary&gt;
        /// Signature verification
        /// &lt;/summary&gt;
        public SigningCredentials SigningCredentials { get; set; }

        /// &lt;summary&gt;
        /// Constructor
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;deniedAction&quot;&gt;Redirect to this API when there is no permission&lt;/param&gt;
        /// &lt;param name=&quot;userPermissions&quot;&gt;User permission collection&lt;/param&gt;
        /// &lt;param name=&quot;deniedAction&quot;&gt;URL of rejected requests&lt;/param&gt;
        /// &lt;param name=&quot;permissions&quot;&gt;Permission collection&lt;/param&gt;
        /// &lt;param name=&quot;claimType&quot;&gt;Claim type&lt;/param&gt;
        /// &lt;param name=&quot;issuer&quot;&gt;Issuer&lt;/param&gt;
        /// &lt;param name=&quot;audience&quot;&gt;Audience&lt;/param&gt;
        /// &lt;param name=&quot;issusedTime&quot;&gt;Issued time&lt;/param&gt;
        /// &lt;param name=&quot;signingCredentials&quot;&gt;Signing verification entity&lt;/param&gt;
        public PermissionRequirement(string deniedAction, Role Role, string claimType, string issuer, string audience, SigningCredentials signingCredentials,long issusedTime, TimeSpan expiration)
        {
            ClaimType = claimType;
            DeniedAction = deniedAction;
            Roles = Role;
            Issuer = issuer;
            Audience = audience;
            Expiration = expiration;
            IssuedTime = issusedTime;
            SigningCredentials = signingCredentials;
        }
    }

③ Implement TokenValidationParameters

Configuration of token information

        public static TokenValidationParameters GetTokenValidationParameters()
        {
            var tokenValida = new TokenValidationParameters
            {
                // Define Token content
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AuthConfig.SecurityKey)),
                ValidateIssuer = true,
                ValidIssuer = AuthConfig.Issuer,
                ValidateAudience = true,
                ValidAudience = AuthConfig.Audience,
                ValidateLifetime = true,
                ClockSkew = TimeSpan.Zero,
                RequireExpirationTime = true
            };
            return tokenValida;
        }

④ Generate Token

Used to store user identity information (Claims) and role authorization information (PermissionRequirement) in the Token.

        /// &lt;summary&gt;
        /// Get a JWT-based Token
        /// &lt;/summary&gt;
        /// &lt;param name=&quot;username&quot;&gt;&lt;/param&gt;
        /// &lt;returns&gt;&lt;/returns&gt;
        public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
        {
            var now = DateTime.UtcNow;
            var jwt = new JwtSecurityToken(
                issuer: permissionRequirement.Issuer,
                audience: permissionRequirement.Audience,
                claims: claims,
                notBefore: now,
                expires: now.Add(permissionRequirement.Expiration),
                signingCredentials: permissionRequirement.SigningCredentials
            );
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            var response = new
            {
                Status = true,
                access_token = encodedJwt,
                expires_in = permissionRequirement.Expiration.TotalMilliseconds,
                token_type = &quot;Bearer&quot;
            };
            return response;
        }

⑤ Implement Service Injection and Authentication Configuration

Import configuration information from other variables, optional

            // Set the key used to encrypt the Token
            // Configure role permissions 
            var roleRequirement = RolePermission.GetRoleRequirement(AccountHash.GetTokenSecurityKey());

            // Define how to generate the user's Token
            var tokenValidationParameters = RolePermission.GetTokenValidationParameters();

Configure ASP.NET Core's identity authentication services

Three configurations need to be implemented:

  • AddAuthorization Import role-based identity authentication strategy
  • AddAuthentication Authentication type
  • AddJwtBearer Jwt authentication configuration
            // Import role-based identity authentication strategy
            services.AddAuthorization(options =&gt;
            {
                options.AddPolicy(&quot;Permission&quot;,
                   policy =&gt; policy.Requirements.Add(roleRequirement));


                // ↓ Authentication type
            }).AddAuthentication(options =&gt;
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

                // ↓ Jwt authentication configuration
            })
            .AddJwtBearer(options =&gt;
            {
                options.TokenValidationParameters = tokenValidationParameters;
                options.SaveToken = true;
                options.Events = new JwtBearerEvents()
                {
                    // Called after the security token and ClaimsIdentity are validated
                    // If the user accesses the logout page
                    OnTokenValidated = context =&gt;
                    {
                        if (context.Request.Path.Value.ToString() == &quot;/account/logout&quot;)
                        {
                            var token = ((context as TokenValidatedContext).SecurityToken as JwtSecurityToken).RawData;
                        }
                        return Task.CompletedTask;
                    }
                };
            });

Inject custom authorization service PermissionHandler

Inject the custom authentication model class roleRequirement

            // Add httpcontext interception
            services.AddSingleton&lt;IAuthorizationHandler, PermissionHandler&gt;();

            services.AddSingleton(roleRequirement);

Add Middleware

I saw an example on the Microsoft website like this... but I tested and found that when the client carried the Token information and the request passed the validation context, it still failed, resulting in a 403 response.

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

I found that it should be like this:

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

Refer to the comments below the articles~

⑥ Implement Login

It is possible to store the APIs that can be accessed when issuing the Token, but this method is not suitable for situations where there are many APIs.

User information (Claims) and role information can be stored, and the backend can obtain the authorized API list through the role information.

        /// <summary>
        /// Login
        /// </summary>
        /// <param name="username">Username</param>
        /// <param name="password">Password</param>
        /// <returns>Token information</returns>
        [HttpPost("login")]
        public JsonResult Login(string username, string password)
        {
            var user = UserModel.Users.FirstOrDefault(x => x.UserName == username && x.UserPossword == password);
            if (user == null)
                return new JsonResult(
                    new ResponseModel
                    {
                        Code = 0,
                        Message = "Login failed!"
                    });
            
            // Configure user claims
            var userClaims = new Claim[]
            {
                new Claim(ClaimTypes.Name, user.UserName),
                new Claim(ClaimTypes.Role, user.Role),
                new Claim(ClaimTypes.Expiration, DateTime.Now.AddMinutes(_requirement.Expiration.TotalMinutes).ToString()),
            };
            _requirement.SetRolesName(user.Role);
            // Generate user identity
            var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);
            identity.AddClaims(userClaims);
            
            var token = JwtToken.BuildJwtToken(userClaims, _requirement);
            
            return new JsonResult(
                new ResponseModel
                {
                    Code = 200,
                    Message = "Login successful! Please remember to save your Token credential!",
                    Data = token
                });
        }

⑦ Add API Authorization Policy

    [Authorize(Policy = "Permission")]

⑧ Implement Custom Authorization Validation

To implement custom API role/policy authorization, you need to inherit AuthorizationHandler<TRequirement>.

The content inside is fully customizable, and AuthorizationHandlerContext is the context for the authentication and authorization, where custom access authorization validation is implemented.

You can also add the function to automatically refresh the Token.

    /// <summary>
    /// Validate user information, perform authorization Handler
    /// </summary>
    public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
                                                       PermissionRequirement requirement)
        {
            List<PermissionRequirement> requirements = new List<PermissionRequirement>();
            foreach (var item in context.Requirements)
            {
                requirements.Add((PermissionRequirement)item);
            }
            foreach (var item in requirements)
            {
                // Validate issuer and audience
                if (!(item.Issuer == AuthConfig.Issuer ?
                    item.Audience == AuthConfig.Audience ?
                    true : false : false))
                {
                    context.Fail();
                }
                // Validate expiration time
                var nowTime = DateTimeOffset.Now.ToUnixTimeSeconds();
                var issued = item.IssuedTime + Convert.ToInt64(item.Expiration.TotalSeconds);
                if (issued < nowTime)
                    context.Fail();
                
                // Check if there is permission to access this API
                var resource = ((Microsoft.AspNetCore.Routing.RouteEndpoint)context.Resource).RoutePattern;
                var permissions = item.Roles.Permissions.ToList();
                var apis = permissions.Any(x => x.Name.ToLower() == item.Roles.Name.ToLower() && x.Url.ToLower() == resource.RawText.ToLower());
                if (!apis)
                    context.Fail();

                context.Succeed(requirement);
                // Redirect to a page when no permission
                // var httpcontext = new HttpContextAccessor();
                // httpcontext.HttpContext.Response.Redirect(item.DeniedAction);
            }

            context.Succeed(requirement);
            return Task.CompletedTask;
        }
    }

⑨ Some Useful Code

Generate hash value from string, for example, for passwords.

For security, remove special characters from the string such as ", ', $.

    public static class AccountHash
    {
        // Get the hash value of a string
        public static string GetByHashString(string str)
        {
            string hash = GetMd5Hash(str.Replace("\"", String.Empty)
                .Replace("'", String.Empty)
                .Replace("$", String.Empty));
            return hash;
        }
        /// <summary>
        /// Get the key used for encrypting Token
        /// </summary>
        /// <returns></returns>
        public static SigningCredentials GetTokenSecurityKey()
        {
            var securityKey = new SigningCredentials(
                new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(AuthConfig.SecurityKey)), SecurityAlgorithms.HmacSha256);
            return securityKey;
        }
        private static string GetMd5Hash(string source)
        {
            MD5 md5Hash = MD5.Create();
            byte[] data = md5Hash.ComputeHash(Encoding.UTF8.GetBytes(source));
            StringBuilder sBuilder = new StringBuilder();
            for (int i = 0; i < data.Length; i++)
            {
                sBuilder.Append(data[i].ToString("x2"));
            }
            return sBuilder.ToString();
        }
    }

Issuing Token

PermissionRequirement is not mandatory, it is used to store role or policy authentication information, Claims should be mandatory.

    /// <summary>
    /// Issue user Token
    /// </summary>
    public class JwtToken
    {
        /// <summary>
        /// Get JWT based Token
        /// </summary>
        /// <param name="username"></param>
        /// <returns></returns>
        public static dynamic BuildJwtToken(Claim[] claims, PermissionRequirement permissionRequirement)
        {
            var now = DateTime.UtcNow;
            var jwt = new JwtSecurityToken(
                issuer: permissionRequirement.Issuer,
                audience: permissionRequirement.Audience,
                claims: claims,
                notBefore: now,
                expires: now.Add(permissionRequirement.Expiration),
                signingCredentials: permissionRequirement.SigningCredentials
            );
            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
            var response = new
            {
                Status = true,
                access_token = encodedJwt,
                expires_in = permissionRequirement.Expiration.TotalMilliseconds,
                token_type = "Bearer"
            };
            return response;
        }

Representing Timestamps

// Unix timestamp
DateTimeOffset.Now.ToUnixTimeSeconds();

// Check if Token is expired
// Convert TimeSpan to Unix timestamp
Convert.ToInt64(TimeSpan);
DateTimeOffset.Now.ToUnixTimeSeconds() + Convert.ToInt64(TimeSpan);

痴者工良

高级程序员劝退师

文章评论