Sep 7, 2018

ASP.NET Core 2.1 Identity- Very simple role management using hardcoded roles in code with command IsInRole & Roles attribute

Update
Make sure to read this first:
https://techbrij.com/asp-net-core-identity-role-policy-authorization
; and this:
https://docs.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-2.1


After spending hours in searching very simple thing and passing few iterations of reducing too complex approach with simpler here are my tips.

Requirement is basic. Application has fixed, hard coded roles like this:



    public class RoleAuthorization

    {

        public const string IdentityApplication = "Identity.Application";



        public const string StudentRole = "STUDENT";

        public const string AdminRole = "ADMIN";

    }



When user is successfully authenticated with standard forms authentication we check in which roles he belongs.

Roles info is persisted in authentication cookie.

Appropriate controllers are protected with standard Authorize attribute like this:



  [Authorize(Roles =RoleAuthorization.StudentRole)]



Further more when needed we can check using User.IsInRole("ADMIN") does authenticated user belongs to given role.

Later I've discovered that this auth scheme name is part of IdentityConstants type:

IdentityConstants.ApplicationScheme

Also it seems that my requirement could be described as "Use cookie authentication without ASP.NET Core Identity" as explained here:

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-2.1&tabs=aspnetcore2x



Let's first look at big picture. There is default Policy. It consist of one or more authentication schemes. These can be different mechanisms apart from standard Cookie. Default authentication scheme name is registered in type CookieAuthenticationDefaults as:

CookieAuthenticationDefaults.AuthenticationScheme

This is key concept. Make sure that all referencing in Authorize attribute, SingIn etc. is done through this constant.
In simpliest form you (optionally) configure only default cookie auth mechanism. This is optional since default policy and its default cookie authentication scheme are already predefined.
I think that default policy name is defined in IdentityConstants type as :

IdentityConstants.ApplicationScheme

But to my knowledge there is no need to reference explicitly anywhere.

To achieve simple cookie configuration with default policy and default authentication scheme put this in your Startup inside ConfigureServices:

 services.AddAuthentication()
                    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>ConfigureCookie(options));
; and optionally configure your authentication rules like this:

   private void ConfigureCookie(CookieAuthenticationOptions options)
        {
            PathString loginUrl = new PathString($"/{nameof(AccountController).DropController()}/{nameof(AccountController.Login)}");
            options.LoginPath = loginUrl;
            options.AccessDeniedPath = loginUrl;         
            options.LogoutPath = loginUrl;
            options.ReturnUrlParameter ="ReturnUrl";
            options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
        }

To achieve registering your authenticated user role(s) inside cookie you need to build claims with appropriate role name and perform singin with this mentioned default auth policy name CookieAuthenticationDefaults.AuthenticationScheme.

Here is excerpt from my working login action:

 [HttpGet]
        public IActionResult Login(string returnUrl = null)
        {
            //If you omit this IF you'll endup with error ERR_TOO_MANY_REDIRECTS
           //since each logout calls again this action
            if (User.Identity.IsAuthenticated)
            {             
                HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            }

            ViewData[CookieAuthenticationDefaults.ReturnUrlParameter] = returnUrl;
            return View();
        }
      [HttpPost]
        public IActionResult Login(LoginViewModel model, string returnUrl = null)
        {         
            ViewData[CookieAuthenticationDefaults.ReturnUrlParameter] = returnUrl;
            if (ModelState.IsValid)
            {
                ApplicationUser user;
                if (!IsAuthenticated(model, out user))
                {
                    return View(model);
                }
                var rolesForUser = _userManager.GetRolesAsync(user).Result;             
                LoginType loginType = LoginType.Undefined;
                string roleName = string.Empty;
                if (rolesForUser.Contains(RoleAuthorization.AdminRole))
                {
                    loginType = LoginType.Admin;
                    roleName = RoleAuthorization.AdminRole;
                }
                else if (rolesForUser.Contains(RoleAuthorization.StudentRole))
                {
                    loginType = LoginType.Student;
                    roleName = RoleAuthorization.StudentRole;
                    var student = _baseRepository.GetByForeignKey<Student>(nameof(Student.ApplicationUserId), user.Id).FirstOrDefault();
                }
                else
                {
                    ModelState.AddModelError("", Resources.SharedResource.ErrorUnauthorized);
                    return View(model);
                }
                CreatePrincipalWithClaimsAndSignIn(model.UserName, roleName, IdentityConstants.ApplicationScheme);
                return RedirectToLocal(returnUrl, loginType);
            }

; and crucial part, creating claims and performing user sign in with default auth policy.



   private void CreatePrincipalWithClaimsAndSignIn(string userName, string role)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);         
            identity.AddClaim(new Claim(ClaimTypes.Name, userName));
            identity.AddClaim(new Claim(ClaimTypes.Role, role));
            var principal = new ClaimsPrincipal(identity);
            HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
        }


You must specify explicitly default auth scheme although one would expect to be used by default since it is only default auth mechanism and method SingInAsync has overload without specifying auth scheme.

//Doesn't work!
          HttpContext.SignInAsync(principal);

All examples I could find pointed to SignInManager class which doesn't support building claims.



Most frustrating is that you can call above SignInAsync without auth policy :



            await HttpContext.SignInAsync( principal);



; and it won't raise exceptions nor perform expected authentication.


Lastly Authorize attribute must be properly decorated.

[Authorize(AuthenticationSchemes = CookieAuthenticationDefaults.AuthenticationScheme, Roles =RoleAuthorization.StudentRole)]


This threw me off path. Note that you DON'T specify POLICY but SCHEME. You must do it even if it is default and only auth scheme in your system. Now you could define one policy with more than one scheme and even more than one policy with its own schemes. It looks that scheme name must be unique.


Here are some helpfull links on subject:

https://stackoverflow.com/questions/45695382/how-do-i-setup-multiple-auth-schemes-in-asp-net-core-2-0

https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-2.1&tabs=aspnetcore2x


https://dzone.com/articles/how-to-add-policy-based-authorization-to-an-aspnet

No comments:

Post a Comment