Token Based Authentication Using Asp.Net Core 2.0

ASP.Net Core 2.0 came out recently and there were quite a few changes to the AJauthentication scheme.  In this article, I’ll talk about how to setup token based authentication using JWT’s in ASP.Net Core 2.0.  We’ll use the Identity system to handle authentication, and Entity Framework to access an MS SQL backend.  NOTE: you will probably need to install the .Net Core 2.0 Libraries.

The source code for this can be found here.

 Setup

  1. Open Visual Studio 2017 and select File -> New -> Project.
  2. From there, select “Asp.NET Core Application” and give a desired name.  (I call it “api”.)

file-select

Next, select “Web API” and no authentication (we’ll set it up manually).

web-api.PNG

This will create a base application.

Entity Framework Setup

Under the project, create a folder named “Models”.  In that folder, create a file named “ApplicationUser.cs” and paste the following:

using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace api.Models
{
    public class ApplicationUser : IdentityUser
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public bool IsEnabled { get; set; }
    }
}

This is the user model used for authentication.

Next, let’s create the application DbContext.  Under the models folder,  add a file named “ApiDbContext” and paste the following:

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace api.Models
{
    public class ApiDbContext : IdentityDbContext
    {
        public ApiDbContext(DbContextOptions options) : base(options)
        {
        }

        protected ApiDbContext()
        {
        }       

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);
        }
    }

}

This sets up the database Db Context which will be used in authentication users.  When the system starts, we can setup a seed user to use for testing.  Let’s add the code for that.

Under models, create a file named “ApiSeedData.cs” and paste the following:

using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using api.Utils;

namespace api.Models
{
    public class ApiDbSeedData
    {
        public ApiDbSeedData(UserManager userManager)
        {

        }

        public static async Task Seed(UserManager userManager, RoleManager roleManager)
        {
            await SeedRolesAndClaims(userManager, roleManager);
            await SeedAdmin(userManager);
        }

        private static async Task SeedRolesAndClaims(UserManager userManager, RoleManager roleManager)
        {

            if (!await roleManager.RoleExistsAsync(Extensions.AdminRole))
            {
                await roleManager.CreateAsync(new IdentityRole
                {
                    Name = Extensions.AdminRole
                });
            }

            if (!await roleManager.RoleExistsAsync(Extensions.UserRole))
            {
                await roleManager.CreateAsync(new IdentityRole
                {
                    Name = Extensions.UserRole
                });
            }

            var adminRole = await roleManager.FindByNameAsync(Extensions.AdminRole);
            var adminRoleClaims = await roleManager.GetClaimsAsync(adminRole);

            if (!adminRoleClaims.Any(x => x.Type == Extensions.ManageUserClaim))
            {
                await roleManager.AddClaimAsync(adminRole, new System.Security.Claims.Claim(Extensions.ManageUserClaim, "true"));
            }
            if (!adminRoleClaims.Any(x => x.Type == Extensions.AdminClaim))
            {
                await roleManager.AddClaimAsync(adminRole, new System.Security.Claims.Claim(Extensions.AdminClaim, "true"));
            }

            var userRole = await roleManager.FindByNameAsync(Extensions.UserRole);
            var userRoleClaims = await roleManager.GetClaimsAsync(userRole);
            if (!userRoleClaims.Any(x => x.Type == Extensions.UserClaim))
            {
                await roleManager.AddClaimAsync(userRole, new System.Security.Claims.Claim(Extensions.UserClaim, "true"));
            }
        }

        private static async Task SeedAdmin(UserManager userManager)
        {
            var u = await userManager.FindByNameAsync("admin");
            if (u == null)
            {
                u = new ApplicationUser
                {
                    UserName = "admin",
                    Email = "admin@nothing.com",
                    SecurityStamp = Guid.NewGuid().ToString(),
                    IsEnabled = true,
                    FirstName = "admin",
                    LastName = "user"
                };
                var x = await userManager.CreateAsync(u, "Admin1234!");
            }
            var uc = await userManager.GetClaimsAsync(u);
            if (!uc.Any(x => x.Type == Extensions.AdminClaim))
            {
                await userManager.AddClaimAsync(u, new System.Security.Claims.Claim(Extensions.AdminClaim, true.ToString()));
            }
            if(!await userManager.IsInRoleAsync(u, Extensions.AdminRole))
                await userManager.AddToRoleAsync(u, Extensions.AdminRole);
        }
    }
}

This uses a constant create the admin claim.  To do this, create a folder named “Utils” and add  a file named “Extensions”. Paste the following;

using Microsoft.AspNetCore.Mvc.ModelBinding;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace api.Utils
{
    public static class Extensions
    {
        public const string AdminClaim = "admin";
        public const string UserClaim = "user";
        public const string ManageUserClaim = "manage_user";
        public const string AdminRole = "admin";
        public const string UserRole = "user";

        public const string RoleClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";

        public static string Error(this ModelStateDictionary modelState)
        {
            foreach (var key in modelState.Keys)
            {
                if (modelState[key].Errors.Count > 0)
                    return modelState[key].Errors[0].ErrorMessage;
            }
            return string.Empty;
        }
    }
}

This sets up the admin claim name and error handling (later).

Now, we need to set the connection string in the appsettings.js file.  Paste the following at the top of the appsettings.js file:

"DefaultConnection": "Data Source=localhost\\SQLEXPRESS; Initial Catalog=api4; Integrated Security=True; MultipleActiveResultSets=True;",

Now, we need to associate this connection string with our database.  Replace startup.cs with the following:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using api.Models;

namespace 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)
        {
            services.AddIdentity(options =>
            {
                options.Password.RequireNonAlphanumeric = false;
            })
            .AddEntityFrameworkStores()
            .AddDefaultTokenProviders();

            var efConnection = Configuration["DefaultConnection"];
            services.AddDbContext(options => options.UseSqlServer(efConnection));

            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)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseMvc();
        }
    }
}

This changes the “ConfigureServices” function (adds identity support and sets our Entity Framework connection string), adds a few using statements at the top of the code.

Next, we need to setup ASP.NET core to run our seed.   Replace Program.cs with the following:


using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Identity;
using api.Models;

namespace api
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = BuildWebHost(args);

            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var userManager = services.GetRequiredService ();
                    var roleManager = services.GetRequiredService();
                    ApiDbSeedData.Seed(userManager, roleManager).Wait();
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService();
                    logger.LogError(ex, "An error occurred while seeding the database.");
                }
            }
            host.Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup()
                .Build();
    }
}

This will run the seed when the program starts up.

Now, let’s setup migrations.  Migrations tracks changes to the database and makes the changes when necessary.  Start off by opening the Package Manager Console but going to Tools -> Nuget Package Manager -> Package Manager Console.

  1. Type in: “Add-Migration” and type in “Initial” for the name.  This will create the first migration for the system.
  2. type in “Update-Database”.  This will create the initial database with our current tables.

The database is ready.

Middleware

Asp.net Core allows the user to create middleware to handle authentication.  We’ll setup the middleware to setup Authenticating and creating JWT’s.

Under Api, create a new folder and name it “Providers”.  Under the Providers folder create a new file and name it “TokenProviderMiddleWare”.  Paste in the following code:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.EntityFrameworkCore;
using api.Models;
using api.Utils;

namespace api.Providers
{
    public class TokenProviderMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly JsonSerializerSettings _serializerSettings;

        public TokenProviderMiddleware(
            RequestDelegate next)
        {
            _next = next;
            _serializerSettings = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented
            };
        }

        public Task Invoke(HttpContext context)
        {
            // If the request path doesn't match, skip
            if (!context.Request.Path.Equals("/api/token", StringComparison.Ordinal))
            {
                return _next(context);
            }

            // Request must be POST with Content-Type: application/x-www-form-urlencoded
            if (!context.Request.Method.Equals("POST")
               || !context.Request.HasFormContentType)
            {
                context.Response.StatusCode = 400;
                return context.Response.WriteAsync("Bad request.");
            }

            return GenerateToken(context);
        }

        private async Task GenerateToken(HttpContext context)
        {
            try
            {
                var username = context.Request.Form["username"].ToString();
                var password = context.Request.Form["password"];

                var signInManager = context.RequestServices.GetService();
                var userManager = context.RequestServices.GetService();

                var result = await signInManager.PasswordSignInAsync(username, password, false, lockoutOnFailure: false);
                if (!result.Succeeded)
                {
                    context.Response.StatusCode = 400;
                    await context.Response.WriteAsync("Invalid username or password.");
                    return;
                }
                var user = await userManager.Users.SingleAsync(i => i.UserName == username);
                if (!user.IsEnabled)
                {
                    context.Response.StatusCode = 400;
                    await context.Response.WriteAsync("Invalid username or password.");
                    return;
                }
                var db = context.RequestServices.GetService();
                var response = GetLoginToken.Execute(user, db);

                // Serialize and return the response
                context.Response.ContentType = "application/json";
                await context.Response.WriteAsync(JsonConvert.SerializeObject(response, _serializerSettings));
            }
            catch (Exception ex)
            {
                //TODO log error
                //Logging.GetLogger("Login").Error("Erorr logging in", ex);
            }
        }

    }

}

In the “Invoke” function, this verifies the end point is “api/token” and then it verifies that it is a POST call.

In the “GenerateToken” function, it verifies the user logs in properly, creates the token and sets it.  The function “GetLoginToken.Execute” creates the actual token and is created next along with a few helper classes.

Under the “Utils” folder, create a class named “LoginResponseData” and paste the following:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace api.Utils
{
    public class LoginResponseData
    {
        public string access_token { get; set; }
        public string refresh_token { get; set; }
        public int expires_in { get; set; }
        public string userName { get; set; }
        public string firstName { get; set; }
        public string lastName { get; set; }
        public bool isAdmin { get; set; }
    }
}

Under the “Utils” folder, create a class named “Configuration” and paste the following:

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace api.Utils
{
    public static class Configuration
    {
        public static IConfigurationRoot Config { get; set; }

        static Configuration()
        {
            var builder = new ConfigurationBuilder()
             .SetBasePath(Directory.GetCurrentDirectory())
            .AddJsonFile("appsettings.json");
            Config = builder.Build();

            Configuration.Config = Config;
        }

        public static string DbConnection => Config["DefaultConnection"];
    }
}

This sets up the Configuration so we can use it to grab appsettings, and it sets up the database connection.

Under the “Providers” folder, create a file named “TokenProviderOptions” and paste in the following:

using Microsoft.IdentityModel.Tokens;
using System;

namespace api.Providers
{
    public class TokenProviderOptions
    {
        /// The relative request path to listen on.
        /// 

        /// The default path is /token.
        public string Path { get; set; } = "api/token";

        ///  The Issuer (iss) claim for generated tokens.
        /// 

        public string Issuer { get; set; }

        /// The Audience (aud) claim for the generated tokens.
        /// 

        public string Audience { get; set; }

        /// The expiration time for the generated tokens.
        /// 

        /// The default is five minutes (300 seconds).
        public TimeSpan Expiration { get; set; } = TimeSpan.FromMinutes(60);

        /// The signing key to use when generating tokens.
        /// 

        public SigningCredentials SigningCredentials { get; set; }

    }
}

These are the options that we will use when creating the JWT token.

Under the “Utils” folder, create a class named “GetLoginToken” and paste the following:


using api.Models;
using api.Providers;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace api.Utils
{
    public class GetLoginToken
    {
        public static TokenProviderOptions GetOptions()
        {
            var signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration.Config.GetSection("TokenAuthentication:SecretKey").Value));

            return new TokenProviderOptions
            {
                Path = Configuration.Config.GetSection("TokenAuthentication:TokenPath").Value,
                Audience = Configuration.Config.GetSection("TokenAuthentication:Audience").Value,
                Issuer = Configuration.Config.GetSection("TokenAuthentication:Issuer").Value,
                Expiration = TimeSpan.FromMinutes(Convert.ToInt32(Configuration.Config.GetSection("TokenAuthentication:ExpirationMinutes").Value)),
                SigningCredentials = new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
            };
        }

        public static LoginResponseData Execute(ApplicationUser user, ApiDbContext db)
        {
            var options = GetOptions();
            var now = DateTime.UtcNow;

            var claims = new List()
            {
                new Claim(JwtRegisteredClaimNames.NameId, user.Id),
                new Claim(JwtRegisteredClaimNames.Jti, user.Id.ToString()),
                new Claim(JwtRegisteredClaimNames.Iat, new DateTimeOffset(now).ToUniversalTime().ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64),
                new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
            };

            var userClaims = db.UserClaims.Where(i => i.UserId == user.Id);
            foreach (var userClaim in userClaims)
            {
                claims.Add(new Claim(userClaim.ClaimType, userClaim.ClaimValue));
            }
            var userRoles = db.UserRoles.Where(i => i.UserId == user.Id);
            foreach(var userRole in userRoles)
            {
                var role = db.Roles.Single(i => i.Id == userRole.RoleId);
                claims.Add(new Claim(Extensions.RoleClaimType, role.Name));
            }

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

            var response = new LoginResponseData
            {
                access_token = encodedJwt,
                expires_in = (int)options.Expiration.TotalSeconds,
                userName = user.UserName,
                firstName = user.FirstName,
                lastName = user.LastName,
                isAdmin = claims.Any(i => i.Type == Extensions.RoleClaimType && i.Value == Extensions.AdminRole)
            };
            return response;
        }
    }

}

This creates the token provider options from appsettings, and sets the login response (setting the encoded JWT).   This also sets the user’s claims and roles.  This also adds a few fields to the appsettings.js file.  Let’s just replace it with the following:

{
  "DefaultConnection": "Data Source=localhost\\SQLEXPRESS; Initial Catalog=api4; Integrated Security=True; MultipleActiveResultSets=True;",
  "Logging": {
    "IncludeScopes": false,
    "Debug": {
      "LogLevel": {
        "Default": "Warning"
      }
    },
    "Console": {
      "LogLevel": {
        "Default": "Warning"
      }
    }
  },
  "TokenAuthentication": {
    "SecretKey": "kW9ys7uuoUfiY8pQyBke7WMhZ2DhuyntGPCVPySuSvc",
    "Issuer": "http://localhost",
    "Audience": "e6b0f93b602544089d3436fabc5b4ab0",
    "ExpirationMinutes": "60",
    "CookieName": "access_token"
  }

}

This is where JWT details are retrieved from.   Secret Key, Issuer, Audience, and ExpirationMinutes are all used for the token.  You can set these to the appropriate settings or generate them yourself.  (In my first talk about JWT authentication, I created a unit test that creates the Secret Key and Issuer.)

Bringing It All Together

Now, we have all of the components to perform token base authentication, we just need to wire it up.  Let’s replace the current startup.cs file with the following:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.EntityFrameworkCore;
using api.Models;
using api.Providers;

namespace 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)
        {
            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", builder =>
                {
                    builder.AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowAnyOrigin()
                    .AllowCredentials();
                });
            });

            services.AddMvc(options =>
            {
            });

            services.AddAuthorization(options =>
            {
                options.AddPolicy("UserManagement", policy => policy.RequireClaim("manage_user"));
                options.AddPolicy("Admin", policy => policy.RequireClaim("admin"));
                options.AddPolicy("User", policy => policy.RequireClaim("user"));
            });

            services.AddIdentity(options =>
            {
                options.Password.RequireNonAlphanumeric = false;
            })
            .AddEntityFrameworkStores()
            .AddDefaultTokenProviders();

            var efConnection = Configuration["DefaultConnection"];
            services.AddDbContext(options => options.UseSqlServer(efConnection));

            // return 401 instead of redirect to login
            services.ConfigureApplicationCookie(options => {
                options.Events.OnRedirectToLogin = context => {
                    context.Response.Headers["Location"] = context.RedirectUri;
                    context.Response.StatusCode = 401;
                    return Task.CompletedTask;
                };
            });

            services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                sharedOptions.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
                      .AddJwtBearer(cfg =>
                      {
                          cfg.RequireHttpsMetadata = false;
                          //cfg.SaveToken = true;

                          cfg.TokenValidationParameters = new TokenValidationParameters()
                          {
                              ValidateIssuer = true,
                              ValidateAudience = true,
                              ValidateLifetime = true,
                              ValidateIssuerSigningKey = true,
                              ValidIssuer = Configuration["TokenAuthentication:Issuer"],
                              ValidAudience = Configuration["TokenAuthentication:Audience"],
                              IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(Configuration["TokenAuthentication:SecretKey"]))
                          };

                          cfg.Events = new JwtBearerEvents
                          {
                              OnAuthenticationFailed = context =>
                              {
                                  Console.WriteLine("OnAuthenticationFailed: " +
                                      context.Exception.Message);
                                  return Task.CompletedTask;
                              },
                              OnTokenValidated = context =>
                              {
                                  Console.WriteLine("OnTokenValidated: " +
                                      context.SecurityToken);
                                  return Task.CompletedTask;
                              }
                          };

                      });

        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        // NOTE: DI is done here
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory,
            ApiDbContext context, UserManager userManager, RoleManager roleManager)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseCors("CorsPolicy");

            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();

            ApiDbSeedData.Seed(userManager, roleManager).Wait();

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

            app.UseMiddleware();
            app.UseAuthentication();

            app.UseMvc();
        }

    }
}<span id="mce_SELREST_start" style="overflow:hidden;line-height:0;"></span>

This sets up the system:

  • lines 33-42 setup the CORS policy so any client can connect.
  • lines 48-43 setup some basic policies (for authentication, more on that later)
  • lines 55-60 setup Identity with our ApplicationUser and ApiDbContext
  • lines 55-56 setup the connection string for the ApiDbContext
  • lines 66-73 make the system return a 401 if the user is not authenticated (normally, it would try to route the user to a login screen, but we don’t want that.)
  • lines 74-111 setup the JWT authentication
  • line 126 tells the system to user our cors policy
  • line 139 sets up our token middleware

Testing

For testing, I use chrome and postman.

  1. Execute the api (F5 or Debug -> Start Debugging)
  2. In Chrome, open postmapostman-1.PNG
  3. Setup the URL to point to api/token (your port may be different)
  4. Set to perform a POST rest call
  5. set to use x-www-form-urlencoded
  6. the the username to admin and password to Admin1234!
  7. click send

If all works correct, you’ll get the access token along with a few other items:

  • refresh_token: this will be used in my next post
  • expires_in: seconds until token expiration
  • username: the username
  • firstname / lastname: should be obvious
  • isAdmin: if the user is an admin (i.e. in admin claim)

Now we can use this token to access restricted data.  The system should’ve setup a controller (under controllers) named “ValuesController”.  If not, then add one.  Let’s also add the “Authorize” attribute so it must be authenticated to work:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;

namespace api.Controllers
{
    [Authorize]
    [Route("api/[controller]")]
    public class ValuesController : Controller
    {
        // GET api/values
        [HttpGet]
        public IEnumerable Get()
        {
            return new string[] { "value1", "value2" };
        }

        // GET api/values/5
        [HttpGet("{id}")]
        public 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)
        {
        }
    }
}

Now, rerun the application and open postman.   If you try to access the values controller endpoint without providing a token, you’ll receive a 401 (unauthorized):

postman-2.PNG

But if you add the token that you received when logging in, it’ll return data:

postman-3.PNG

Success!   We successfully generated a JWT which we can use to authorize through the system.   Furthermore, I setup policies and roles and the current user is in the Admin Policy and Admin Role so if you change the authorize attribute to [Authorize(Policy=”admin”)]  (or if you want to use roles then do: [Authorize(Roles=”admin”)]) then the user should still be able to authenticate against that policy.  If you set the policy (or role) to “User” then we should get a 403 error.

My next talk will explain how to implement refresh tokens in asp.net core 2.0.  This can be found here.

UPDATE:  Somebody made a request to add an endpoint that authenticates (instead of using middleware).  The latest version on GitHub now contains this endpoint.

First, I added a new controller to handle authentication:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using api.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using api.Utils;

namespace api.Controllers
{
    [Produces("application/json")]
    [Route("api/Authentication")]
    [AllowAnonymous]
    public class AuthenticationController : Controller
    {
        private readonly SignInManager _signInManager;
        private readonly UserManager _userManager;
        private readonly ApiDbContext _db;

        public AuthenticationController(SignInManager signInManager,
                UserManager userManager,
                ApiDbContext db)
        {
            _signInManager = signInManager;
            _userManager = userManager;
            _db = db;
        }

        [HttpPost]
        [Route("")]
        public async Task Authenticate(string username, string password)
        {
            if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
                return BadRequest("username and password may not be empty");

            var result = await _signInManager.PasswordSignInAsync(username, password, false, lockoutOnFailure: false);
            if (!result.Succeeded)
            {
                return BadRequest("Invalid username or password.");
            }
            var user = await _userManager.Users
                .SingleAsync(i => i.UserName == username);
            if (!user.IsEnabled)
            {
                return BadRequest("Invalid username or password.");
            }
            var response = GetLoginToken.Execute(user, _db);
            return Ok(response);
        }
    }
}
  • This creates an endpoint at ‘api/authentication’.
  • The sign-in-manager, user-manager, and the database are injected via ASP.Net Core2.
  • Line 35 creates the endpoint which authenticates the user.  It requires the username and password and it verifies this with the sign-in-manager and then it verifies that the user is enabled (via a call to the user manager).  After that, it creates a token (this part is used in the middle-ware as well), and returns the response.

Testing this in Postman, when the correct username and password are entered, it returns the token (among other things), just like the middleware:

postman-4

 

7 thoughts on “Token Based Authentication Using Asp.Net Core 2.0

  1. I need to issue a refresh token. I’m porting my existing web api from Asp.Net 4 and my token provider issues refresh tokens. Any guidance on how to do this in asp.net core 2?

    Like

  2. Hi AJ, This is a super helpful series. I’m using it to get up to speed with ASP.NET Core and, hopefully, get around a long-blocking issue for me with authentication for the backend for a Xamarin-based app.

    Any chance you can help me convert your “TokenProviderMIddleware” class to an api controller? I’m not sure if that’s the solution I need but the problem I am having is that when I run my implementation of your code none of my attempts to test /api/token will complete because the resource is not found (404). My assumption is that the problem is I have the ASP.NET Core project type that is “MVC with entities” … so I have Views, too. I’m guessing this is messing with your assumption/approach about routes versus the default approach for an MVC oriented project but since MS claims MVC projects also support API projects I hoped things still might be workable. . Which is why I guessed that, maybe(?) I’d be better off to have “login” (aka token) controller.

    Any help or suggestions appreciated. And thanks!
    Dave G

    Like

    1. Hi David,
      The class that handles creating the token is named “GetLoginToken” and the function is “Execute”. if you move this call from the TokenProviderMiddleware into a controller endpoint then you should be good.

      The controller endpoint would need to take the username / password, and when it is hit, it’ll need to verify these via SignInManager> class. This is injectable, so you can have it set on your constructor.

      I hope this helps.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s