ASP.NET Core policy authorization and ABP authorization

c55020d233563114c69b70e1c3bc8d13.png

Policy authorization in ASP.NET Core

First let’s create a WebAPI application.

Then introduce the Microsoft.AspNetCore.Authentication.JwtBearer package.

Strategy

In the ConfigureServices method of the Startup class, the form of adding a policy is as follows:

services.AddAuthorization(options =>
    {
        options.AddPolicy("AtLeast21", policy =>
            policy.Requirements.Add(new MinimumAgeRequirement(21)));
    });

Here we go step by step.

services.AddAuthorization is used to add authorization methods. Currently, only AddPolicy is supported.

In ASP.NET Core, there are three authorization forms based on roles, claims, and policies, all of which use AddPolicy to add authorization processing.

Among them, there are two APIs as follows:

public void AddPolicy(string name, AuthorizationPolicy policy);
        public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy);
  • name = "AtLeast21", where “AtLeast21” is the name of the strategy.

  • policy.Requirements.Add() is used to add a policy tag (to store the data of this policy). This tag needs to inherit the IAuthorizationRequirement interface.

How should the name of the policy be set? How to write policies and use Requirements.Add() for authorization?

Let’s put it here for a moment, and we’ll explain it next.

Define a Controller

Let’s add a Controller:

[ApiController]
    [Route("[controller]")]
    public class BookController : ControllerBase
    {
        private static List<string> BookContent = new List<string>();
        [HttpGet("Add")]
        public string AddContent(string body)
        {
            BookContent.Add(body);
            return "success";
        }

        [HttpGet("Remove")]
        public string RemoveContent(int n)
        {
            BookContent.Remove(BookContent[n]);
            return "success";
        }

        [HttpGet("Select")]
        public List<object> SelectContent()
        {
            List<object> obj = new List<object>();
            int i = 0;
            foreach (var item in BookContent)
            {
                int tmp = i;
                i + + ;
                obj.Add(new { Num = tmp, Body = item });
            }
            return obj;
        }

        [HttpGet("Update")]
        public string UpdateContent(int n, string body)
        {
            BookContent[n] = body;
            return "success";
        }
    }

The function is very simple, it is to add, delete, check and modify the contents of the list.

Set permissions

Earlier we created BookController, which has the function of adding, deleting, checking and modifying. There should be a permission set for each function.

In ASP.NET Core, a permission tag needs to inherit the IAuthorizationRequirement interface.

Let’s set five permissions:

Add a file and fill in the following code.

/*
     IAuthorizationRequirement is an empty interface. For specific authorization requirements, its attributes and other information are customized.
     The inheritance relationship here also makes no sense.
     */

    // Permission to access Book
    public class BookRequirment : IAuthorizationRequirement
    {
    }

    //Add, delete, check and modify Book permissions
    // You can inherit IAuthorizationRequirement or BookRequirment
    public class BookAddRequirment : BookRequirment
    {
    }
    public class BookRemoveRequirment : BookRequirment
    {
    }
    public class BookSelectRequirment : BookRequirment
    {
    }
    public class BookUpdateRequirment : BookRequirment
    {
    }

BookRequirment represents the ability to access BookController, and the other four represent the permissions to add, delete, check, and modify.

Define strategy

After setting the permissions, we start setting the policy.

In Startup’s ConfigureServices, add:

services.AddAuthorization(options =>
            {
                options.AddPolicy("Book", policy =>
                {
                    policy.Requirements.Add(new BookRequirment());
                });

                options.AddPolicy("Book:Add", policy =>
                {
                    policy.Requirements.Add(new BookAddRequirment());
                });

                options.AddPolicy("Book:Remove", policy =>
                {
                    policy.Requirements.Add(new BookRemoveRequirment());
                });

                options.AddPolicy("Book:Select", policy =>
                {
                    policy.Requirements.Add(new BookSelectRequirment());
                });

                options.AddPolicy("Book:Update", policy =>
                {
                    policy.Requirements.Add(new BookUpdateRequirment());
                });

            });

Here we only set one permission for each strategy. Of course, each strategy can add multiple permissions.

The names here are separated by :, mainly for readability, so that people can know the hierarchical relationship at a glance.

Storing user information

For the sake of simplicity, a database is not used here.

The following user information structure is written casually. User-role-the permissions the role has.

This permission can be stored in any type. As long as you can identify which permission it is.

/// <summary>
    /// Store user information
    /// </summary>
    public static class UsersData
    {
        public static readonly List<User> Users = new List<User>();
        static UsersData()
        {
            //Add an administrator
            Users.Add(new User
            {
                Name = "admin",
                Email = "[email protected]",
                Role = new Role
                {
                    Requirements = new List<Type>
                    {
                        typeof(BookRequirment),
                        typeof(BookAddRequirment),
                        typeof(BookRemoveRequirment),
                        typeof(BookSelectRequirment),
                        typeof(BookUpdateRequirment)
                    }
                }
            });

            // No delete permission
            Users.Add(new User
            {
                Name = "Author",
                Email = "wirter",
                Role = new Role
                {
                    Requirements = new List<Type>
                    {
                        typeof(BookRequirment),
                        typeof(BookAddRequirment),
                        typeof(BookRemoveRequirment),
                        typeof(BookSelectRequirment),
                    }
                }
            });
        }
    }

    public class User
    {
        public string Name { get; set; }
        public string Email { get; set; }
        public Role Role { get; set; }
    }

    /// <summary>
    /// It is OK to store the policy authorization of the role here, string numbers, etc., as long as it can store the representation.
    /// <para>There is no meaning here, it is just a way of identification</param>
    /// </summary>
    public class Role
    {
        public List<Type> Requirements { get; set; }
    }

Mark access rights

After defining the policy, it is time to mark access permissions for Controllers and Actions.

Use the [Authorize(Policy = "{string}")] attributes and properties to set the permissions required to access this Controller and Action.

Here we set it up separately, marking each function with a permission (the minimum granularity should be a function, not an API).

[Authorize(Policy = "Book")]
    [ApiController]
    [Route("[controller]")]
    public class BookController : ControllerBase
    {
        private static List<string> BookContent = new List<string>();

        [Authorize(Policy = "Book:Add")]
        [HttpGet("Add")]
        public string AddContent(string body){}

        [Authorize(Policy = "Book:Remove")]
        [HttpGet("Remove")]
        public string RemoveContent(int n){}

        [Authorize(Policy = "Book:Select")]
        [HttpGet("Select")]
        public List<object> SelectContent(){}

        [Authorize(Policy = "Book:Update")]
        [HttpGet("Update")]
        public string UpdateContent(int n, string body){}
    }

Authentication: Token credentials

Because WebAPI is used, Bearer Token authentication is used. Of course, Cookies, etc. can also be used. You can use any authentication method.

//Set the verification method to Bearer Token
            // Add using Microsoft.AspNetCore.Authentication.JwtBearer;
            // You can also use the string "Brearer" instead of JwtBearerDefaults.AuthenticationScheme
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234")), // Key for encryption and decryption of Token

                        // Whether to verify the publisher
                        ValidateIssuer = true,
                        // Publisher name
                        ValidIssuer = "server",

                        // Whether to verify subscribers
                        // Subscriber name
                        ValidateAudience = true,
                        ValidAudience = "client007",

                        // Whether to verify the token validity period
                        ValidateLifetime = true,
                        // Each time a token is issued, the token is valid for
                        ClockSkew = TimeSpan.FromMinutes(120)
                    };
                });

The above code is a template and can be modified at will. The authentication method here has nothing to do with our policy authorization.

Issue login credentials

The following Action is placed in BookController as a login function. This part is not important either. It mainly issues credentials to users and identifies users. A user’s Claim can store this user’s unique identifier.

/// <summary>
        /// User logs in and issues credentials
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        [AllowAnonymous]
        [HttpGet("Token")]
        public string Token(string name)
        {
            User user = UsersData.Users.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
            if (user is null)
                return "This user was not found";

            //Define user information
            var claims = new Claim[]
            {
                new Claim(ClaimTypes.Name, name),
                new Claim(JwtRegisteredClaimNames.Email, user.Email)
            };

            //Same as the configuration in Startup
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234"));

            JwtSecurityToken token = new JwtSecurityToken(
                issuer: "server",
                audience: "client007",
                claims: claims,
                notBefore: DateTime.Now,
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
            );

            string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
            return jwtToken;
        }

Add the following two lines to Configure:

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

Custom authorization

Custom authorization needs to inherit the IAuthorizationHandler interface. The class that implements this interface can decide whether to authorize user access.

The implementation code is as follows:

/// <summary>
    /// Determine whether the user has permissions
    /// </summary>
    public class PermissionHandler : IAuthorizationHandler
    {
        public async Task HandleAsync(AuthorizationHandlerContext context)
        {
            // Permissions required to currently access Controller/Action (policy authorization)
            IAuthorizationRequirement[] pendingRequirements = context.PendingRequirements.ToArray();

            // Get user information
            IEnumerable<Claim> claims = context.User?.Claims;

            // Not logged in or unable to get user information
            if (claims is null)
            {
                context.Fail();
                return;
            }


            // Get username
            Claim userName = claims.FirstOrDefault(x => x.Type == ClaimTypes.Name);
            if (userName is null)
            {
                context.Fail();
                return;
            }
            // ... omit some inspection processes ...

            // Get this user's information
            User user = UsersData.Users.FirstOrDefault(x => x.Name.Equals(userName.Value, StringComparison.OrdinalIgnoreCase));
            List<Type> auths = user.Role.Requirements;

            // Check one by one
            foreach (IAuthorizationRequirement requirement in pendingRequirements)
            {
                // If this permission is not found in the user permission list
                if (!auths.Any(x => x == requirement.GetType()))
                    context.Fail();

                context.Succeed(requirement);
            }

            await Task.CompletedTask;
        }
    }

process:

  • Get user information (context.User) from context (Context)

  • Get the role this user belongs to and the permissions this role has

  • Get the permissions required by the Controller/Action of this request (context.PendingRequirements)

  • Check the required permissions (foreach loop) whether this user has them all

Finally, you need to register this interface and service into the container:

services.AddSingleton<IAuthorizationHandler, PermissionHandler>();

After doing this, you can test the authorization.

IAuthorizationService

The class that implemented the IAuthorizationHandler interface earlier is used to customize whether the user has the right to access this Controller/Action.

The IAuthorizationService interface is used to determine whether authorization is successful. It is defined as follows:

public interface IAuthorizationService
    {
        Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements);

        Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName);
    }

The DefaultAuthorizationService interface implements IAuthorizationService, and ASP.NET Core uses DefaultAuthorizationService by default to confirm authorization.

Earlier we used the IAuthorizationHandler interface to customize authorization. If we go one level deeper, it can be traced back to IAuthorizationService.

DefaultAuthorizationService is the default implementation of IAuthorizationService. There is a piece of code as follows:

5eb267210f7b235729d950f95b77a879.png

DefaultAuthorizationService is more complicated. Generally, we only need to implement IAuthorizationHandler.

Reference: https://docs.microsoft.com/zh-cn/dotnet/api/microsoft.aspnetcore.authorization.defaultauthorizationservice?view=aspnetcore-3.1

ABP Authorization

We have already introduced the policy authorization in ASP.NET Core. Here we will introduce the authorization in ABP. We continue to use the ASP.NET Core code that has been implemented previously.

Create ABP application

Nuget installs Volo.Abp.AspNetCore.Mvc, Volo.Abp.Autofac.

Create the AppModule class with the following code:

[DependsOn(typeof(AbpAspNetCoreMvcModule))]
    [DependsOn(typeof(AbpAutofacModule))]
    public class AppModule : AbpModule
    {
        public override void OnApplicationInitialization(
            ApplicationInitializationContext context)
        {
            var app = context.GetApplicationBuilder();
            var env = context.GetEnvironment();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseRouting();
            app.UseConfiguredEndpoints();
        }
    }

Add .UseServiceProviderFactory(new AutofacServiceProviderFactory()) to the Host of Program. The example is as follows:

public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .UseServiceProviderFactory(new AutofacServiceProviderFactory())
            ...
            ...

Then in the ConfiguraServices method in Startup, add the ABP module and set it to use Autofac.

public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplication<AppModule>(options=>
            {
                options.UseAutofac();
            });
        }

Define permissions

The PermissionDefinitionProvider class is used in ABP to define permissions and create a class with the following code:

public class BookPermissionDefinitionProvider : PermissionDefinitionProvider
    {
        public override void Define(IPermissionDefinitionContext context)
        {
            var myGroup = context.AddGroup("Book");
            var permission = myGroup.AddPermission("Book");
            permission.AddChild("Book:Add");
            permission.AddChild("Book:Remove");
            permission.AddChild("Book:Select");
            permission.AddChild("Book:Update");
        }
    }

A group Book is defined here, and a permission Book is defined. Book has four sub-permissions under it.

Remove services.AddAuthorization(options =>... in Startup.

Move the remaining dependency injection service code into the AppModule’s ConfigureServices.

Change Startup’s Configure to:

app.InitializeApplication();

Configure in AbpModule is changed to:

var app = context.GetApplicationBuilder();
            var env = context.GetEnvironment();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();
            app.UseRouting();

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

            app.UseConfiguredEndpoints();

PermissionHandler needs to be changed to:

public class PermissionHandler : IAuthorizationHandler
    {
        public Task HandleAsync(AuthorizationHandlerContext context)
        {
            // Permissions required to currently access Controller/Action (policy authorization)
            IAuthorizationRequirements[] pendingRequirements = context.PendingRequirements.ToArray();

            // Check one by one
            foreach (IAuthorizationRequirement requirement in pendingRequirements)
            {
                context.Succeed(requirement);
            }


            return Task.CompletedTask;
        }
    }

Delete the UserData file; the BookController needs to modify the login and credentials.

For specific details, please refer to the warehouse source code: https://github.com/whuanles/2020-07-12

e5e7115b5894deec78bbeb3f4a5684ec.png

4662cf852ac6710218a40b7a97767563.png

8bcdf0871e6f5a130357f912e073983a.png

ba86a38cb7e410b4e909dd4cc747c373.png

18b2f9818363a7fd732516c048df6742.jpeg

98bdfc2b8f4861150913eb879d90a676.png