Default .Net Core Identity vs. JwtBaerer Authentication custom set up



 Using the default asp.net core 2.1 identity is made to be pretty simple. Not only simple but the microsoft team have even removed the AccountContoller class as well as Login and Register pages, that have been there for many years. They have incorporated the identification logic in the Razor Class Library.
 This significant change has make it very easy for developers that don't want to deal with authentication and they are even happier because encapsulated like that the Identity is even more frendly for beginners.

Now creating a new project with indiviual authentication looks like that:

default-dot-net-identity
The new .net core project structure

 The first time when I saw it I was sure that I have been missed to choose the Indiviual authentication option and I don't have one. Fast recreate of the project convinced me that there is something more here.
 Amazingly for me, the mentioned Identity parts were gone. So now what?

How to create custom logic on login and register?


 In our .net core templates we use a custom authentication with JwtBaerer authentication . Doing that complicates the project a bit, but gives us the option to make custom logic which meets our business requirements. In order to that we dig a little bit inside the identity configurations, but nothing to scary. The changes that we create could be summed up to these:
  • 1. Create the old fellow the AccountController
  • 2. Create TokenProviderOptions
  • 3. Create TokenProviderMiddleware
  • 4. Create TokenProviderExtensions
  • 5. Update the Startup class

1. Create the old fellow the AccountController

It consists of Register functionallity which is quite trivial and uses the default .Net Core user creation.

 [AllowAnonymous]
    public class AccountController : BaseController
    {
        private readonly UserManager userManager;

        public AccountController(UserManager userManager)
        {
            this.userManager = userManager;
        }

        [HttpPost]
        public async Task Register([FromBody]UserRegisterBindingModel model)
        {
            if (model == null || !this.ModelState.IsValid)
            {
                return this.BadRequest(this.ModelState.GetFirstError());
            }

            var user = new ApplicationUser { Email = model.Email, UserName = model.Email };
            var result = await this.userManager.CreateAsync(user, model.Password);

            if (result.Succeeded)
            {
                return this.Ok();
            }

            return this.BadRequest(result.GetFirstError());
        }
    }

Registering users is vital part of the application. One may want to add lots of different checkups and logic on this point. Now it has the option to that in the Register api call.

2. Create TokenProviderOptions

This class is intended to keep the information needed for the creation of Jwt (Json web token) authentication. It looks like that:

 public class TokenProviderOptions
    {
        public string Path { get; set; } = "/token";

        public string Issuer { get; set; }

        public string Audience { get; set; }

        public TimeSpan Expiration { get; set; } = TimeSpan.FromDays(15);

        public Func<Task<string>> NonceGenerator { get; set; } = () => Task.FromResult(Guid.NewGuid().ToString());

        public SigningCredentials SigningCredentials { get; set; }
    }

3.Create TokenProviderMiddleware

 The concept of the middlewares is strongly incorporated in .net core. The middlewares are componets that's assembled into an app pipeline to handle requests and responses. Each component can chooses whether to pass the request to the next component in the pipeline or can perform work before and after the next component in the pipeline is invoked.

 public class TokenProviderMiddleware
    {
        private readonly RequestDelegate next;
        private readonly TokenProviderOptions options;
        private readonly Func<HttpContext, Task<GenericPrincipal>> principalResolver;

        public TokenProviderMiddleware(
            RequestDelegate next,
            IOptions<TokenProviderOptions>  options,
            Func<HttpContext, Task<GenericPrincipal>> principalResolver)
        {
            this.next = next;
            this.options = options.Value;
            this.principalResolver = principalResolver;
        }

        public Task Invoke(HttpContext context)
        {
            if (!context.Request.Path.Equals(this.options.Path, StringComparison.Ordinal))
            {
                return this.next(context);
            }

            if (context.Request.Method.Equals("POST") && context.Request.HasFormContentType)
            {
                return this.GenerateToken(context);
            }

            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
            return context.Response.WriteAsync("Bad request");
        }

        private static int GetClaimIndex(IList claims, string type)
        {
            for (var i = 0; i < claims.Count; i++)
            {
                if (claims[i].Type == type)
                {
                    return i;
                }
            }

            return -1;
        }

        private async Task GenerateToken(HttpContext context)
        {
            var principal = await this.principalResolver(context);
            if (principal == null)
            {
                context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
                await context.Response.WriteAsync("Invalid email or password.");
                return;
            }

            var now = DateTime.UtcNow;
            var unixTimeSeconds = (long)Math.Round(
                (now.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);

            var existingClaims = principal.Claims.ToList();

            var systemClaims = new List
            {
                new Claim(JwtRegisteredClaimNames.Sub, principal.Identity.Name),
                new Claim(JwtRegisteredClaimNames.Jti, await this.options.NonceGenerator()),
                new Claim(JwtRegisteredClaimNames.Iat, unixTimeSeconds.ToString(), ClaimValueTypes.Integer64)
            };

            foreach (var systemClaim in systemClaims)
            {
                var existingClaimIndex = GetClaimIndex(existingClaims, systemClaim.Type);
                if (existingClaimIndex < 0)
                {
                    existingClaims.Add(systemClaim);
                }
                else
                {
                    existingClaims[existingClaimIndex] = systemClaim;
                }
            }

            var jwt = new JwtSecurityToken(
                issuer: this.options.Issuer,
                audience: this.options.Audience,
                claims: existingClaims,
                notBefore: now,
                expires: now.Add(this.options.Expiration),
                signingCredentials: this.options.SigningCredentials);

            var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);

            var response = new
            {
                access_token = encodedJwt,
                expires_in = (int)this.options.Expiration.TotalMilliseconds,
                roles = existingClaims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value)
            };

            context.Response.ContentType = GlobalConstants.JsonContentType;
            await context.Response.WriteAsync(JsonConvert.SerializeObject(response));
        }
    }
 When invoked this middleware generates new JwtSecurityToken, which is returned to the client. Except for the token also some meta information is returned to the client like the Issuer, the audience and etc. This information is checked also when the token is being validated on calls that require authentication. The expire time of the token is also put in the client response.

4. Create TokenProviderExtensions

To be able to use to token easily it can be set up as an extenction to IApplicationBuilder.

public static class TokenProviderExtensions
    {
        public static void UseJwtBearerTokens(
            this IApplicationBuilder app,
            IOptions<TokenProviderOptions> options,
            Func<HttpContext, Task<GenericPrincipal>> principalResolver)
        {
            ValidateArgs(app, options, principalResolver);

            app.UseMiddleware<TokenProviderMiddleware>(options, principalResolver);

            app.UseAuthentication();
        }

        private static void ValidateArgs(
            IApplicationBuilder app,
            IOptions<TokenProviderOptions> options,
            Func<HttpContext, Task<GenericPrincipal>> principalResolver)
        {
            if (app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }

            if (options?.Value == null)
            {
                throw new ArgumentNullException(nameof(options));
            }

            if (principalResolver == null)
            {
                throw new ArgumentNullException(nameof(principalResolver));
            }
        }
    }

 Our new middleware is set up right before the the regular Authentication middleware in the UseJwtBearerTokens method.

5. Update the Startup class

 Up to now, we have only created the new infrastucture, but we haven't use it yet. All these classes are combined in the Startup.cs class. Firstly in the ConfigureServices method we should create the options for the middleware and set up them to the service provider.

 var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.configuration["JwtTokenValidation:Secret"]));

            services.Configure<TokenProviderOption>(opts =>
            {
                opts.Audience = this.configuration["JwtTokenValidation:Audience"];
                opts.Issuer = this.configuration["JwtTokenValidation:Issuer"];
                opts.Path = "/api/account/login";
                opts.Expiration = TimeSpan.FromDays(15);
                opts.SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
            });

            services
                .AddAuthentication()
                .AddJwtBearer(opts =>
                {
                    opts.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = signingKey,
                        ValidateIssuer = true,
                        ValidIssuer = this.configuration["JwtTokenValidation:Issuer"],
                        ValidateAudience = true,
                        ValidAudience = this.configuration["JwtTokenValidation:Audience"],
                        ValidateLifetime = true
                    };
                });

            services
                .AddIdentity<ApplicationUser, ApplicationRole>(options =>
                {
                    options.Password.RequiredLength = 6;
                    options.Password.RequireDigit = false;
                    options.Password.RequireLowercase = false;
                    options.Password.RequireNonAlphanumeric = false;
                    options.Password.RequireUppercase = false;
                })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddUserStore<ApplicationUserStore>()
                .AddRoleStore<ApplicationRoleStore>()
                .AddDefaultTokenProviders();
 Here we also set up the configurations for the size of the password as well as the different validations of the token. For example rather the issuer of the token to be checked or not and etc. This gives us freedom of setting up different logic depending of the requirements. Once we do that we can use our extention method from the previous point and set up the middleware with the desired parameters in the Configure method:

    app.UseJwtBearerTokens(
                app.ApplicationServices.GetRequiredService>(),
                PrincipalResolver);
 The principal resolver is a method that is also in the Startup.cs file, which is only called when user logs in. The method simply checkes the Username and the Password of the user.



        private static async Task<GenericPrincipal> PrincipalResolver(HttpContext context)
        {
            var email = context.Request.Form["email"];

            var userManager = context.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
            var user = await userManager.FindByEmailAsync(email);
            if (user == null || user.IsDeleted)
            {
                return null;
            }

            var password = context.Request.Form["password"];

            var isValidPassword = await userManager.CheckPasswordAsync(user, password);
            if (!isValidPassword)
            {
                return null;
            }

            var roles = await userManager.GetRolesAsync(user);

            var identity = new GenericIdentity(email, "Token");
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id));

            return new GenericPrincipal(identity, roles.ToArray());
        }




 As a conclusion I can say that using the default .Net core project authentication can only help you for small and relativly not complex projects, because at some point the logic around the Authentication and Authorization gets more complex, and if you have chosen the easy way you may have a problem.  Using the custom logic leaves you with the opportunity for easier extensibility.