Developers Club geek daily blog

1 year, 10 months ago
Life in Las Vegas is not limited to gamblings. Despite glory of the gambling capital, pass also actions absolutely from other spheres of life here. In particular, the annual DEVIntersection conference which this year was visited by command of our developers. And here we want to tell about all the most important and interesting that they learned at conference.

Changes in ASP.NET 5


In the context of ASP.NET 5 all components of a stack were updated or completely rewritten. Changes did not avoid also ASP.NET Identity that cannot but please. Including, also the system of authorization was updated, and these are really high-quality changes (IMHO). Further we will consider what were entered changes into authentication systems and authorizations.

ASP.NET Identity


On Habré there was already a review article on ASP.NET Identity therefore we will pass the prolog.

ASP.NET Identity provides tools for work with users and their authentication. This system consists of Microsoft.AspNet.Identity library with the description of the main classes and abstractions in the form of interfaces of storage of users, etc. Microsoft.AspNet.Identity.EntityFramework — library which contains implementation of a user/role and storages on the basis of Entity Framework 7.

The first what there is a wish to pay attention, this lack of interfaces for users and roles to. Identity does not dictate to us what has to be our user. At setup it is enough to specify what classes of the user and a role we want to use. It is reached because the class UserManager delegates to storage reading/record of properties of the user. For example, such operations of a class UserManager, as GetUserName, SetUserName and ChangePassword lead to challenges of the same methods at UserStore.

Storages of users and roles, shall not describe all possible operations. In one application it is enough to user to have only a name, the password and a possibility of an input on them, and in another it is necessary to have different options of authentication and to support a great lot of other opportunities. For the application it is enough to us to select the suitable interface of storage and to implement only it.

The list of interfaces for storage of users:

  • IUserLoginStore<TUser>
  • IUserRoleStore<TUser>
  • IUserClaimStore<TUser>
  • IUserPasswordStore<TUser>
  • IUserSecurityStampStore<TUser>
  • IUserEmailStore<TUser>
  • IUserLockoutStore<TUser>
  • IUserPhoneNumberStore<TUser>
  • IQueryableUserStore<TUser>
  • IUserTwoFactorStore<TUser>

In a simple case we have enough only IUserStore and IUserPasswordStore.
  • IUserStore — management of a name and user's ID.
  • IUserPasswordStore — installation and reading the password.

ASP.NET 5 provides the infrastructure for setup of the pipeline of the application and its services (the built-in DI container).

Connection of services:

public void ConfigureServices(IServiceCollection services)
{
    // more here

    // регистрация сервисов Identity
    services.AddIdentity<ApplicationUser, IdentityRole>()
        // регистрация UserStore/RoleStore на основе EF
        .AddEntityFrameworkStores<ApplicationDbContext>()
        // регистрация стандартных токен провайдеров 
        .AddDefaultTokenProviders();

    // more here
}

Middleware-authentication activation:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // more here
    app.UseIdentity(); // настройка Cookie Auth Middleware
    // more here
}

It is default setting.

At the expense of a modularity and use of the DI ASP.NET, Identity allows to replace the components without complete rewriting. For example, we want to change hashing of passwords, the user's validator (checking uniqueness, etc.), to connect the storages of users and roles, and also to specify the Claim type for storage of roles:

services.AddScoped<IPasswordHasher<User>, MyPasswordHasher>();

services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
    options.Password.RequireDigit = true;
    options.Password.RequiredLength = 8;
    options.SignIn.RequireConfirmedEmail = true;
    options.ClaimsIdentity.RoleClaimType = "MyRoleClaimType";
})
    .AddUserStore<MyStore>()
    .AddRoleStore<MyRoleStore>()
    .AddUserValidator<MyUserValidator>()
    .AddDefaultTokenProviders();


Many Identity components have a separate method of expansion for setup, but not all. As well as in a case with IPasswordHasher, we have no expansion method. In that case, for registration we need to register service of a hesher of passwords before it makes Identity.

Configuring of Identity is performed with the help now Action<IdentityOptions>, as it is given in an example above.

It is possible to notice that separation of responsibility improved. UserManager is engaged only in management of users, delegating specific objectives on other components, such as storage of users, a hesher of passwords, the user's validator. For configuring there is no need to replace specific classes, for this purpose we use IdentityOptions.

One more small note on Identity: considering experience Identity 2 and a problem with performance by search of users, the class of the user can have properties NameNormilized and EmailNormalized. Class UserManager when updating the user automatically updates also this field, using the interface IUserStore. It is made to avoid a challenge of functions UPPER() on the party of a DB. Besides, different storages can run for case-sensitive search, and in the presence of a field with the normalized line it is possible to run for search irrespective of storage.

In general it is possible to note that influence of Identity on an application code decreased. The system became more modular and changed/expanded due to adaptation to ASP.NET 5. From the point of view of use, it is practically the same system, as earlier, with usual mechanics of work. But there is also a tar spoon: there was an old problem which is present still with MembershipProvider (.NET 2.0): if our storage does not describe some of interfaces, then by a challenge of the corresponding method at UserManager we will receive NotSupportedException.

Further we will pass to authentication


In an example above, in a method Configure, we call an expansion method app.UseIdentity(). Under a cowl of this method there is a connection of middleware, setup of a payplayn cookie of authentication:

var options = app.ApplicationServices.GetRequiredService<IOptions<IdentityOptions>>().Value;
app.UseCookieAuthentication(options.Cookies.ExternalCookie);
app.UseCookieAuthentication(options.Cookies.TwoFactorRememberMeCookie);
app.UseCookieAuthentication(options.Cookies.TwoFactorUserIdCookie);
app.UseCookieAuthentication(options.Cookies.ApplicationCookie);

In turn, UseCookieAuthentication connects middleware:

app.UseMiddleware<CookieAuthenticationMiddleware>(options);

For interaction with middleware of authentication it is necessary to use a class AuthenticationManager, which hangs on HttpContext. Let's say we do not want to use Identity for management of our users, and we prefer to do it independently:

// SignOut для текущего пользователя
var authenticationDescriptions = HttpContext.Authentication.GetAuthenticationSchemes();
foreach (var authenticationDescription in authenticationDescriptions)
{
    HttpContext.Authentication.SignOutAsync(authenticationDescription.AuthenticationScheme);
}

// Аутентифицированный пользователь в ASP.NET представлен в виде ClaimsIdentity
// Создаем ClaimsIdentity и добавляем необходимые данные
var claims = new List<Claim>
        {
            new Claim(ClaimTypes.NameIdentifier, "123"),
            new Claim(ClaimsIdentity.DefaultNameClaimType, "MyUserName"),
            new Claim(ClaimTypes.Role, "Administrators"),
            new Claim("Lastname", "MyLastName"),
            new Claim("email", "bob@smith.com")
        };

var id = new ClaimsIdentity(claims, "MyCookiesScheme", ClaimsIdentity.DefaultNameClaimType, ClaimTypes.Role);
var principal = new ClaimsPrincipal(id);

// Аутентификация ClaimsPrincipal
HttpContext.Authentication.SignInAsync("MyCookiesScheme", new ClaimsPrincipal(id));

Approximately according to the same scheme works and SignInManager from ASP.NET Identity. At authentication we need to specify by means of what authentication scheme we loginit the user. On the basis of this parameter AuthenticationManager defines with what middleware it is necessary to interact.

The scheme of work is almost identical to MVC 5 and OWIN.

Authorization


If for Identity and authentication the general principle of use remained approximately the same, then authorization underwent much more significant changes. And to the best!

In applications with a large number of roles there is a problem of use of attribute [Authorize] and checks of a type user.IsInRole. At change of safety requirements modification of the application is to extremely labor-consuming and subject errors process. It is necessary to find all places where this or that role is used, or it is necessary to find and change all places where there is an appeal to a certain function of the application. I.e. the code of authorization is blurred according to the application.

Let's start. In addition to checks on membership of the user, in roles we had new instruments of authorization. On default attribute [Authorize] now accepts not the list of roles, and "policy".

(attribute [Authorize(Roles=””)] it is supported for the sake of backward compatibility)

services.AddAuthorization(options =>
{
    options.AddPolicy("SuperAdministrationPolicy", policy =>
    {
        policy.RequireRole("Admins");
        policy.RequireClaim("adminlevel", "Level1", "Level2");
    });
    options.AddPolicy("RegularAdministrationPolicy", policy =>
    {
        policy.RequireRole("Admins");
    });
});

AuthorizationPolicy comprises a set Requirements (requirements). By default we have Requirements for check of roles, kleym and user name. It is also possible to describe Requirement in the form of a separate class:

public class AdminLevelRequirement : AuthorizationHandler<AdminLevelRequirement>, IAuthorizationRequirement
{
    private readonly string _adminLevel;

    public AdminLevelRequirement(string adminLevel)
    {
        _adminLevel = adminLevel;
    }

    protected override void Handle(AuthorizationContext context, AdminLevelRequirement requirement)
    {
        var user = context.User;
        if (user.IsInRole("Admins") &&user.HasClaim("adminlevel", _adminLevel))
        {
            context.Succeed(requirement);
        }
    }
}

// использование requirement описанного в виде класса
options.AddPolicy("AdminLevel1Policy", policy =>
                {
                    policy.AddRequirements(new AdminLevelRequirement("Level1"));
                });

In this example we inherit AuthorizationHandler<> and IAuthorizationRequirement, but it is not obligatory. Here AdminLevelRequirement describes both the requirement, and the processor of the requirement. In a simple case this quite suitable solution. But if more flexibility is necessary, then we can separate requirements and processors. The class of our requirement (Requirement) needs to be inherited from IAuthorizationRequirement (the marker interface), and it will bear in itself(himself) only the description of parameters of the requirement. For example, administrator's level, its permission access level. In turn, classes for processing of requirements inherit AuthorizationHandler<> also contain only methods Handle().

Example:

// класс описывающий требование
public class AdminLevelRequirement : IAuthorizationRequirement
{
    public string AdminLevel { get; }

    public AdminLevelRequirement(string adminLevel)
    {
        AdminLevel = adminLevel;
    }
}
// обработчик требования
public class AdminLevelRequirementHandler : AuthorizationHandler<AdminLevelRequirement>
{
    protected override void Handle(AuthorizationContext context, AdminLevelRequirement requirement)
    {
        var user = context.User;
        if (user.IsInRole("Admins") &&user.HasClaim("adminlevel", requirement.AdminLevel))
        {
            context.Succeed(requirement);
        }
    }
}
// добавление требования к политике
policy.AddRequirements(new AdminLevelRequirement("Level1"));
// добавление обработчиков требования в коллекцию сервисов приложения
services.AddInstance<IAuthorizationHandler>(new AdminLevelRequirementHandler());

The processor can return AuthorizationContext.Suceed() or AuthorizationContext.Fail(). If we described several processors for one requirement, we can execute them by the principle of OR or AND. For execution in OR style, processors can return only Succeed(). If the logic of processing on AND is necessary, then we can use return Fail(). I.e. if in processors is never caused Fail(), at successful check of any of processors access will be provided to the user.

We pass to use.

Use the politician of authorization by a declarative method by means of attribute:

[Authorize("PoliсyName")]
public IActionResult ActionName()

Authorization service which can be received by the declaration of dependence in the class is added to tools:

// класс описывающий требование
public class HomeController : Controller
{
    private readonly IAuthorizationService _authorizationService;


    public HomeController(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }
}

Let's use service for check of compliance of the user to policy of authorization:

if (await _authorizationService.AuthorizeAsync(User, "MyPolicy"))

In addition to security policies, authorization on the basis of resources (Resource Based Authorization) is available to us. It is applied in cases when authorization depends on object to which access is provided. For use of this type of authorization we need to implement the handler who processes certain Requirement/Policy and a resource:

public class TenantAuthorizationHandler : AuthorizationHandler<OperationAuthorizationRequirement, Tenant>
{
    protected override void Handle(AuthorizationContext context,
        OperationAuthorizationRequirement requirement,
        Tenant resource)
    {
        if (requirement.Name == "Update")
        {
            if (!context.User.HasClaim("adminlevel", "Level1"))
            {
                context.Fail();
                return;
            }

            if (!context.User.HasClaim("region", resource.Region))
            {
                context.Fail();
                return;
            }

            context.Succeed(requirement);
            return;
        }

        context.Fail();
    }
}

Such processor differs from normal only in the fact that accepts also a copy of a resource to which access is made.

OperationAuthorizationRequirement — it is a requirement class by default which has only Name property. This class can be used for simple scripts and checks of access for the user for the named operations, for example Read, Update, Delete and so on. By itself, in this case we can describe also own classes of requirements.

New approach allows is centralized to manage authorization and minimizes need to change a code of the main application at change of requirements to safety. All code with rules of access can be taken out in separate assembly and to encapsulate logic of authorization.

Useful links:

This article is a translation of the original post at habrahabr.ru/post/273753/
If you have any questions regarding the material covered in the article above, please, contact the original author of the post.
If you have any complaints about this article or you want this article to be deleted, please, drop an email here: sysmagazine.com@gmail.com.

We believe that the knowledge, which is available at the most popular Russian IT blog habrahabr.ru, should be accessed by everyone, even though it is poorly translated.
Shared knowledge makes the world better.
Best wishes.

comments powered by Disqus