Token based authentication and Identity framework in ASP.NET Core - Part 2

Setting up token based authentication in ASP.NET Core application on top of Identity Framework

In article Token based authentication and Identity framework in ASP.NET Core - Part 1 I described how to setup identity library for storing user accounts. Now we are going to setup ASP.NET Core Web API project to issue the token for authenticated users so they can access protected resources.

Identity Web API instance will be separated from from the resource Web API, so we'll have eventually two and possibly in future more resource services which will all use single identity server fr authentication and issuing access tokens.

Services Digram

The main benefit in this approach with distributed identity and token provider service is that authentication is completely isolated and it is not mixed with the business logic of the solution. Any change in the rest of the solution does not affect identity service which makes is less vulnerable and more stable.

Note

Code for this article is available on GitHub as a public repository so you are free to fork or download it https://github.com/dejanstojanovic/dotnetcore-token-authentication

Setting up Token based authentication

First we are going to create ASP.NET Core Web API project and choose Web API project template. This will create a project structure as a kick start for us to re-configure it. Before any code change we need to add all the necessary NuGet packages in the project file

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <Folder Include="wwwroot\" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.0" />
    <PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="2.1.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\Sample.Core.Identity.Data\Sample.Core.Identity.Data.csproj" />
  </ItemGroup>

</Project>

    

You see that one of the references is our identity library developed in the previous part of of token based authentication in ASP.NET Core. We are going to use our previously developed library for storing and retrieving the data of the user.

Let's switch to code now and setup all the dependency injection and the pipeline in the Startup.cs class

using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using Sample.Core.Identity.Data.DbContexts;
using Sample.Core.Identity.Data.Enities;
namespace Sample.Core.Identity.Api
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
#region Add CORS
services.AddCors(options => options.AddPolicy("Cors", builder =>
{
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
}));
#endregion
#region Add Entity Framework and Identity Framework
services.AddDbContext<ApplicationUserDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DatabaseConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationUserDbContext>();

            #endregion

            #region Add Authentication
            var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Tokens:Key"]));
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            }).AddJwtBearer(config =>
            {
                config.RequireHttpsMetadata = false;
                config.SaveToken = true;
                config.TokenValidationParameters = new TokenValidationParameters()
                {
                    IssuerSigningKey = signingKey,
                    ValidateAudience = true,
                    ValidAudience = this.Configuration["Tokens:Audience"],
                    ValidateIssuer = true,
                    ValidIssuer = this.Configuration["Tokens:Issuer"],
                    ValidateLifetime = true,
                    ValidateIssuerSigningKey = true
                };
            });
            #endregion


            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseCors("Cors");
            app.UseAuthentication();


            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseMvc();

        }
    }
}

    

We are ready now to write our authentication logic now

Implementing token based authentication logic

Before we write any code for our account controller we need to define the models which structures we are going to use for user authentication and registration. Identity and user management is a a big subject so we are going to stick to the basics and only develop user account registration and authentication. 

Login model is a pretty simple one with only two fields, username and password

using System;
using System.ComponentModel.DataAnnotations;

namespace Sample.Core.Identity.Api.Models
{
    public class LoginModel
    {
        [Required]
        public String Username { get; set; }

        [Required]
        public String Password { get; set; }
    }
}

    

Since login model properties are subset of user registration model, we can inherit login model in registration model to avoid code repetition

using System;
using System.ComponentModel.DataAnnotations;

namespace Sample.Core.Identity.Api.Models
{
    public class RegisterModel:LoginModel
    {
        [Required]
        [StringLength(200)]
        public String FirstName { get; set; }

        [Required]
        [StringLength(250)]
        public String LastName { get; set; }

        [Required]
        [EmailAddress]
        public String Email { get; set; }

        [Required]
        [Compare("Password")]
        [RegularExpression("^((?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])|(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[^a-zA-Z0-9])|(?=.*?[A-Z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])|(?=.*?[a-z])(?=.*?[0-9])(?=.*?[^a-zA-Z0-9])).{8,}$", ErrorMessage = "Passwords must be at least 8 characters and contain at 3 of 4 of the following: upper case (A-Z), lower case (a-z), number (0-9) and special character (e.g. !@#$%^&*)")]
        public String PasswordConfirmation { get; set; }
    }
}

    

Some basic validation is also set so we do not have to make trip to database for the invalid data posted to the service endpoint.

Note

Model validation is an important part of application as it blocks code execution and heavy unnecessary operations on the invalid models

We are ready to write our controller now, but first let's put some configuration as we are going to need some token settings:

  • Private key (Key used to encrypt the token data - mandatory)
  • Token lifetime (Lifetime of the token which can be validated on the resource server - optional)
  • Issuer (Service/Url of the service issued the token which can e used on the resource server for validation - optional)
  • Audience (Urls of the resource services for which token can is issued for. This is also for the validation on the resource server - optional)

Lets now start with our core component which is AccountController class

using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using Sample.Core.Identity.Api.Models;
using Sample.Core.Identity.Data.Enities;

namespace Sample.Core.Identity.Api.Controllers
{
    [Produces("application/json")]
    [Route("api/[controller]")]
    [ApiController]
    [AllowAnonymous]
    public class AccountController : ControllerBase
    {
        readonly UserManager<ApplicationUser> userManager;
        readonly SignInManager<ApplicationUser> signInManager;
        readonly IConfiguration configuration;
        readonly ILogger<AccountController> logger;


        public AccountController(
           UserManager<ApplicationUser> userManager,
           SignInManager<ApplicationUser> signInManager,
           IConfiguration configuration,
           ILogger<AccountController> logger)
        {
            this.userManager = userManager;
            this.signInManager = signInManager;
            this.configuration = configuration;
            this.logger = logger;
        }


        [HttpPost]
        [Route("token")]
        public async Task<IActionResult> CreateToken([FromBody] LoginModel loginModel)
        {
            if (ModelState.IsValid)
            {
                var loginResult = await signInManager.PasswordSignInAsync(loginModel.Username, loginModel.Password, isPersistent: false, lockoutOnFailure: false);

                if (!loginResult.Succeeded)
                {
                    return BadRequest();
                }

                var user = await userManager.FindByNameAsync(loginModel.Username);

                return Ok(GetToken(user));
            }
            return BadRequest(ModelState);

        }

        [Authorize]
        [HttpPost]
        [Route("refreshtoken")]
        public async Task<IActionResult> RefreshToken()
        {
            var user = await userManager.FindByNameAsync(
                User.Identity.Name ??
                User.Claims.Where(c => c.Properties.ContainsKey("unique_name")).Select(c => c.Value).FirstOrDefault()
                );
            return Ok(GetToken(user));

        }


        [HttpPost]
        [Route("register")]
        [AllowAnonymous]
        public async Task<IActionResult> Register([FromBody] RegisterModel registerModel)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    //TODO: Use Automapper instaed of manual binding

                    UserName = registerModel.Username,
                    FirstName = registerModel.FirstName,
                    LastName = registerModel.LastName,
                    Email = registerModel.Email
                };

                var identityResult = await this.userManager.CreateAsync(user, registerModel.Password);
                if (identityResult.Succeeded)
                {
                    await signInManager.SignInAsync(user, isPersistent: false);
                    return Ok(GetToken(user));
                }
                else
                {
                    return BadRequest(identityResult.Errors);
                }
            }
                return BadRequest(ModelState);
            
            
        }

        private String GetToken(IdentityUser user)
        {
            var utcNow = DateTime.UtcNow;

            var claims = new Claim[]
            {
                        new Claim(JwtRegisteredClaimNames.Sub, user.Id),
                        new Claim(JwtRegisteredClaimNames.UniqueName, user.UserName),
                        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
                        new Claim(JwtRegisteredClaimNames.Iat, utcNow.ToString())
            };

            var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.configuration.GetValue<String>("Tokens:Key")));
            var signingCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256);
            var jwt = new JwtSecurityToken(
                signingCredentials: signingCredentials,
                claims: claims,
                notBefore: utcNow,
                expires: utcNow.AddSeconds(this.configuration.GetValue<int>("Tokens:Lifetime")),
                audience: this.configuration.GetValue<String>("Tokens:Audience"),
                issuer: this.configuration.GetValue<String>("Tokens:Issuer")
                );

            return new JwtSecurityTokenHandler().WriteToken(jwt);

        }

    }
}
    

We'll strict to only three basic methods for 

  • Registering an account
  • Authenticate to get the token
  • Refresh the token

Testing authentication with Postman

Registering a new account

Now, since we have basic functionality we can test it using Postman. First we need to register an account to test the token issuing, so we'll start with posting register model

Register Request

Note

For account registration endpoint, consider using Google reCAPTCHA validation or any other anti-bot mechanism to avoid fake accounts generated by bot. More on implementing reCAPTCHA you can find at http://bit.ly/2L3jRKx

When we register and account, we will get a token as a response which we can use as an authentication on the resource service.

Register Response

Issuing a token for username account credentials

We are going to focus more on how to use this token later when we work on an resource service with authentication protected endpoints. Since we have an existing account we can now test our token issuing for the user credentials (login).

Login Request

Login Response

Getting new token for already authenticated user

And finally we test token refresh by providing our current token which we previously get with calling token endpoint with our account credentials. Since token authentication is carried in request header values, we are not going to define any message body. Instead we are only going to add Authorization header key with value Bearer <acquired token>

Refresh Token

This is an ideal example how to protect your endpoint. Since all configuration is done in Startup.cs, all you need is to decorate controller action with Authorize attribute.

Protected resource service

Since we already have an authorized endpoint for refreshing the token, we can re-use same approach for authenticated endpoints on the resource web api service. We are going to have to do the same Startup.cs setup and add the same configuration. We can optionally connect to identity database to pull user data upon authentication, but if we only want to validate the token we can keep our resource server not connected to the database.

The Startup.cs authentication configuration of the resorce service is the same as the one in identification service since they use the same symmetric key. We are going to re-use the Values controller which is added by Visual Studio

using System.Collections.Generic;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace Sample.Core.Resource.Asymetric.Api.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    [Authorize]
    public class ValuesController : ControllerBase
    {
        // GET api/values
        [HttpGet]
        public ActionResult<IEnumerable<string>> Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public ActionResult<string> Get(int id)
        {
            return "value";
        }

        // POST api/values
        [HttpPost]
        public void Post([FromBody] string value)
        {
        }

        // PUT api/values/5
        [HttpPut("{id}")]
        public void Put(int id, [FromBody] string value)
        {
        }

        // DELETE api/values/5
        [HttpDelete("{id}")]
        public void Delete(int id)
        {
        }
    }
}

    

If we try to hit /api/values endpoint without authentication token, we'll get 401 Unauthorized. We first need to get the token from our authorization service and then use that token to call resource service endpoint.

In ideal scenario, resource server should not be able to sign any token which introduces the usage of asymmetric keys or certificates. We are going to discus this in a separate article.

Complete code of resource service sample Web API you can find in this github repository https://github.com/dejanstojanovic/dotnetcore-token-authentication

If you want to know more about asymmetric token authentication check out Token based authentication and Identity framework in ASP.NET Core - Part 3 article

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article