Asp.Net Core with Extended Identity and Jwt Auth Walkthrough

 


Quite often I think of an idea for an app where I would have a website and a mobile app that share the same functionality. Every time I start to prototype something like this I get stuck on authentication. When you build a web application in Asp.Net Core, it is really easy to set up the built-in Identity platform to allow users to login, but if you want to allow a mobile app to access APIs on the same site, that takes a lot more work. Of course, you can use a cloud-based authentication system like AzureAd or Firebase authentication, but if you arent housing any sensitive data, that's a lot of extra plumbing that takes away from building your application. 

This walkthrough will show you how to create an Asp.Net core site with Asp.Net Core Identity for authentication and authorization. We will be extending the identity to include additional user information and roles. We will also add jwt authentication and authorization, so SPA's and mobile apps can use the API's and share the backend code from the website. For simplicity, we will only be using a single project for this demo. In most cases, you would want to separate your logic into multiple projects, but for simplicity in this demo, I decided to stick with a single project. We will also be using the dotnet CLI to do a lot of the work since it is easier to document the process that way, and it also allows you to use either visual studio or visual studio code.

Configuring the Project

Let's start by creating the project. Open a terminal window in a folder where you store your projects and type the following command.

dotnet new mvc --auth Individual -uld -o AspNetCoreCustomIdentyJwtDemo

This will create an asp.net project with the identity module all built-in. The database will be configured for localdb, and you can edit that in the appsettings.json file in the root of your project. Now type the following command in your terminal to get to the root.

cd AspNetCoreCustomIdentyJwtDemo

we will be using Entity Framework migrations in this project, so you will need dotnet ef installed. If you haven't installed the tools, run the following command to install it locally.

dotnet tool install --global dotnet-ef

Right now all the identity pages are compiled into binaries, so they can't be edited. The next step is to scaffold the razor pages so that we can customize the identity pages. To do this, the first thing we need to do is install the code generator. In your terminal window run the following command.

dotnet tool install -g dotnet-aspnet-codegenerator

With the code generator installed, there are also several NuGet packages that need to be installed. In visual studio, you could use the NuGet package manager, but we are going to install them with the dotnet cli. In your terminal window execute the following commands.


dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design

This is the code generation tool for scaffolding controllers and the identity UI


dotnet add package Microsoft.EntityFrameworkCore.Design

This provides design-time tools for entity framework for use with code first migrations.


dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

This package allows Identity to use the entity framework.


dotnet add package Microsoft.AspNetCore.Identity.UI

This package provides the ASP.Net Core Razor Pages build for the UI


dotnet add package Microsoft.EntityFrameworkCore.SqlServer

This package is used to the entity framework to connect to SQL Server


dotnet add package Microsoft.EntityFrameworkCore.Tools

This package adds the commands necessary for working with the database via migrations


Adding the ASP.NET Core Identity UI

Now we should be ready to scaffold our pages. Run the following command in the terminal windows.

dotnet aspnet-codegenerator identity -dc ApplicationDbContext

This will create all the razor pages with code behind for managing the Asp.Net Core Identity system. You can find the generated code in the Areas\Identity\Pages\Account\Manage directory.

One of the caveats I have run into with scaffolding this way is it rebuilds some of the startup configurations in the Areas\Identity\IdentityHostingStartup.cs file. To fix the issue open the file in the editor and replace the class definition with the following code.

public class IdentityHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices((context, services) =>
            {
            });
        }
    }

You will also need to remove the following using directive at the top of the Areas\Identity\IdentityHostingStartup.cs file.

using AspNetCoreCustomIdentyJwtDemo.Areas.Identity.Data;

You will also need to delete the Areas\Identity\Data folder. This has a second DbContext which will conflict with the primary one.

Creating The Custom User and Role Classes

Now we need to add some files. Unfortunately, this can't be done with the CLI, so we will have to do it from the IDE. On the models folder, right-click and select add a new file, and create the following files.

  • ApplicationUser.cs
  • ApplicationRole.cs
  • ApplicationUserRole.cs

After creating the files, we need to add the following code to each of the corresponding files.

ApplicationUser.cs

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class ApplicationUser : IdentityUser<Guid>
    {
        public ApplicationUser() : base()
        {
        }

        public string FirstName { get; set; }
        public string LastName { get; set; }

        public ICollection<ApplicationUserRole> UserRoles { get; set; }
    }
}

This model inherits from the IdentityUser and adds a FirstName and LastName property to the user definition. We also add the ApplicationUserRole as a collection to map the roles to the user. The Guid in the generic constructor of the base class defines the primary key as a guid which is preferable to an integer when making an application scalable.

ApplicationRole.cs

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class ApplicationRole : IdentityRole<Guid>
    {
        public ApplicationRole() : base()
        {
        }

        public ApplicationRole(string roleName) : base(roleName)
        {
        }

        public ApplicationRole(string roleName, string description, DateTime creationDate) : base(roleName)
        {
            this.Description = description;
            this.CreationDate = creationDate;
        }

        public string Description { get; set; }
        public DateTime CreationDate { get; set; }

        public ICollection<ApplicationUserRole> UserRoles { get; set; }
    }
}

The ApplicationRole adds a Description and CreationDate property to the role and creates the link to the user with the ApplicationUserRole collection.

ApplicationUserRole.cs

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class ApplicationUserRole : IdentityUserRole<Guid>
    {
        public virtual ApplicationUser User { get; set; }
        public virtual ApplicationRole Role { get; set; }
    }
}

This will create the join table necessary for creating the many to many relationships for the users to roles.

Next, we need to update the data/ApplicationDbContext.cs file with information about the custom identity and role tables.

ApplicationDbContext.cs

using System;
using System.Collections.Generic;
using System.Text;
using AspNetCoreCustomIdentyJwtDemo.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;


namespace AspNetCoreCustomIdentyJwtDemo.Data
{
    public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid, IdentityUserClaim<Guid>,
        ApplicationUserRole, IdentityUserLogin<Guid>,
        IdentityRoleClaim<Guid>, IdentityUserToken<Guid>>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.Entity<ApplicationUserRole>(userRole =>
            {
                userRole.HasKey(ur => new { ur.UserId, ur.RoleId });

                userRole.HasOne(ur => ur.Role)
                    .WithMany(r => r.UserRoles)
                    .HasForeignKey(ur => ur.RoleId)
                    .IsRequired();

                userRole.HasOne(ur => ur.User)
                    .WithMany(r => r.UserRoles)
                    .HasForeignKey(ur => ur.UserId)
                    .IsRequired();
            });
        }

    }
}

The applicationDbContext inherits from the IdentityContext which adds Identity configuration to the standard DbContext which the IdentityContext inherits from. The generic constructor is where the identity context is configured to use our custom user and role classes. The onModelCreating method is where we use fluent syntax to build the many to many relationships between the ApplicationUser and the ApplicationRole.

if you want to add data to initialize the database with users and roles, create a SeedData.cs file in the Data directory and add the following code.

SeedData.cs

using System;
using System.Threading.Tasks;
using AspNetCoreCustomIdentyJwtDemo.Models;
using Microsoft.AspNetCore.Identity;

namespace AspNetCoreCustomIdentyJwtDemo.Data
{
    public class SeedData
    {
        public static async Task Initialize(ApplicationDbContext context,
                              UserManager<ApplicationUser> userManager,
                              RoleManager<ApplicationRole> roleManager)
        {
            context.Database.EnsureCreated();

            string adminRole = "Admin";
            string adminDesc = "This is the administrator role";

            string userRole = "User";
            string userDesc = "This is the default User role";

            string password = "Password123!";

            if (await roleManager.FindByNameAsync(adminRole) == null)
            {
                await roleManager.CreateAsync(new ApplicationRole(adminRole, adminDesc, DateTime.Now));
            }
            if (await roleManager.FindByNameAsync(userRole) == null)
            {
                await roleManager.CreateAsync(new ApplicationRole(userRole, userDesc, DateTime.Now));
            }
            

            if (await userManager.FindByNameAsync("TestAdmin") == null)
            {
                var user = new ApplicationUser
                {
                    UserName = "TestAdmin",
                    Email = "TestAdmin@email.com",
                    FirstName = "Test",
                    LastName = "Admin",
                    PhoneNumber = "1234567890",
                    EmailConfirmed = true
                };

                var result = await userManager.CreateAsync(user);
                if (result.Succeeded)
                {
                    await userManager.AddPasswordAsync(user, password);
                    await userManager.AddToRoleAsync(user, adminRole);
                }
                
            }

            if (await userManager.FindByNameAsync("TestUser") == null)
            {
                var user = new ApplicationUser
                {
                    UserName = "TestUser",
                    Email = "TestUser@email.com",
                    FirstName = "Test",
                    LastName = "User",
                    PhoneNumber = "0987654321",
                    EmailConfirmed = true
                };

                var result = await userManager.CreateAsync(user);
                if (result.Succeeded)
                {
                    await userManager.AddPasswordAsync(user, password);
                    await userManager.AddToRoleAsync(user, userRole);
                }
                
            }

            
        }  
    }
}

Initializing data can be done in the ApplicationContext using a builder, but since we need to assign users and passwords, we use a custom class that implements the identity usermanager.

Configuring The Custom Identity

Next, we need to change the following lines to the startup.cs file in the root of the project. In the ConfigureServices method replace the services.AddDefaultIdentity section with the following code.

services.AddIdentity<ApplicationUser, ApplicationRole>(
                    options =>
                    {
                        options.SignIn.RequireConfirmedEmail = true;
                        options.Stores.MaxLengthForKeys = 128;
                    })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

This section configures the identity to use our custom classes and adds some options. The RequireConfirmedEmail parameter means that a user will have to confirm their email via a link sent to their email address before their ID will be active. The MaxLengthForKeys parameter sets the bit length for security keys. These are the default values provided by the template.

You will need to add the missing import for the ApplicationUser and ApplicationRole, so add the following to the top of the file.

using AspNetCoreCustomIdentyJwtDemo.Models;

next, add the following to the parameters of the Configure method in the startup.cs file, so it looks like this

public void Configure(IApplicationBuilder app, 
    IWebHostEnvironment env, ApplicationDbContext context,
    RoleManager<ApplicationRole> roleManager,
    UserManager<ApplicationUser> userManager)

Now add the following line as the last line in the Configure method of the startup.cs

SeedData.Initialize(context, userManager, roleManager).Wait();

When you are done editing the startup.cs, it should look like this.

using AspNetCoreCustomIdentyJwtDemo.Data;
using AspNetCoreCustomIdentyJwtDemo.Models;
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.Extensions.Hosting;

namespace AspNetCoreCustomIdentyJwtDemo
{
    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.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, ApplicationRole>(
                    options =>
                    {
                        options.SignIn.RequireConfirmedEmail = true;
                        options.Stores.MaxLengthForKeys = 128;
                    })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddControllersWithViews();
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app,
            IWebHostEnvironment env, ApplicationDbContext context,
            RoleManager<ApplicationRole> roleManager,
            UserManager<ApplicationUser> userManager)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });

            SeedData.Initialize(context, userManager, roleManager).Wait();
        }
    }
}

There are some edits we need to make in the UI elements to make this work.

First, we need to edit the Views/Shared/_LoginPartial.cshtml file and change these lines:

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

to

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

Now edit the Views/_ViewImports.cshtml file so it looks like this:

@using AspNetCoreCustomIdentyJwtDemo
@using AspNetCoreCustomIdentyJwtDemo.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, AspNetCoreCustomIdentityJwtDemo

Building the Database with Migrations

Another caveat of creating the identity using the scaffolding is that it prebuilds migrations that cause an error when you customize the user and role. You can create a migration and edit the up config to delete the constraints on the user first, but the easier way is to delete all the files in the data\migrations folder and start fresh.

After deleting the migration files we need to add a new migration create the database tables with our custom properties. In the terminal type the following command.

dotnet ef migrations add CustomIdentity --context ApplicationDbContext

This command will use all of our models that are configured using the DbSet in the ApplicationDbContext and script the table creation. It will also add all the Identity tables that were defined using the scaffolding.

Now run the following command to update the database.

dotnet ef database update --context ApplicationDbContext

This command actually builds the database using the default connection string located in the appsettings.json file. The database will contain a __EFMigrationsHistory table that the system will use to track migrations. The next time a migration is created this table will be checked, and only new models will be used to generate objects in the migration.

You should be able to build and launch the site now using the dotnet run command. The default website is either https://localhost:5001 for an https site or https://localhost:5000 for a standard http site.

If you try to register or login, you will get an error because we still have to update the pages to use our new user objects.

Editing the Identity UI Razor Pages

Edit the Areas\Identity\Pages\Account\Manage\Login.cshtml.cs file and replace all instances of IdentityUser with ApplicationUser. Using a search and replace would make this pretty easy.

Now we will need to add an import for our model, so add the following to the top of the file.

using AspNetCoreCustomIdentyJwtDemo.Models;

Next, we want to change the InputModelClass with the following.

public class InputModel
        {
            [Required]
            [Display(Name = "User ID")]
            public string UserName { get; set; }

            [Required]
            [DataType(DataType.Password)]
            public string Password { get; set; }

            [Display(Name = "Remember me?")]
            public bool RememberMe { get; set; }
        }

Now we want to change the parameter in this line.

var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);

To

var result = await _signInManager.PasswordSignInAsync(Input.UserName, Input.Password, Input.RememberMe, lockoutOnFailure: false);

Now we need to edit the Areas\Identity\Pages\Account\Manage\Login.cshtml file and replace the Input.Email with Input.UserName

<div class="form-group">
                    <label asp-for="Input.UserName"></label>
                    <input asp-for="Input.UserName" class="form-control" />
                    <span asp-validation-for="Input.UserName" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.Password"></label>
                    <input asp-for="Input.Password" class="form-control" />
                    <span asp-validation-for="Input.Password" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <div class="checkbox">
                        <label asp-for="Input.RememberMe">
                            <input asp-for="Input.RememberMe" />
                            @Html.DisplayNameFor(m => m.Input.RememberMe)
                        </label>
                    </div>
                </div>

Now we need to make similar edits to the registration page.

Edit the Areas\Identity\Pages\Account\Manage\Register.cshtml.cs file and replace all instances of IdentityUser with ApplicationUser. Using a search and replace would make this pretty easy.

Now we will need to add an import for our model, so add the following to the top of the file.

using AspNetCoreCustomIdentyJwtDemo.Models;

Next, we want to change the InputModel Class with the following.

public class InputModel
        {
            [Required]
            [Display(Name = "User ID")]
            public string UserName { get; set; }

            [Required]
            [Display(Name = "First Name")]
            public string FirstName { get; set; }

            [Required]
            [Display(Name = "Last Name")]
            public string LastName { get; set; }

            [Display(Name = "Phone Number")]
            [Phone]
            public string PhoneNumber { get; set; }

            [Required]
            [EmailAddress]
            [Display(Name = "Email")]
            public string Email { get; set; }

            [Required]
            [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
            [DataType(DataType.Password)]
            [Display(Name = "Password")]
            public string Password { get; set; }

            [DataType(DataType.Password)]
            [Display(Name = "Confirm password")]
            [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
            public string ConfirmPassword { get; set; }
        }

Now replace this line.

var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };

with

var user = new ApplicationUser
                {
                    UserName = Input.UserName,
                    Email = Input.Email,
                    FirstName = Input.FirstName,
                    LastName = Input.LastName,
                    PhoneNumber = Input.PhoneNumber
                };

Now we need to update the UI form. Open the Areas\Identity\Pages\Account\Manage\Register.cshtml.cs file and replace the form with the following.

<form asp-route-returnUrl="@Model.ReturnUrl" method="post">
            <h4>Create a new account.</h4>
            <hr />
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="Input.UserName"></label>
                <input asp-for="Input.UserName" class="form-control" />
                <span asp-validation-for="Input.UserName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.FirstName"></label>
                <input asp-for="Input.FirstName" class="form-control" />
                <span asp-validation-for="Input.FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.LastName"></label>
                <input asp-for="Input.LastName" class="form-control" />
                <span asp-validation-for="Input.LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.Email"></label>
                <input asp-for="Input.Email" class="form-control" />
                <span asp-validation-for="Input.Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.PhoneNumber"></label>
                <input asp-for="Input.PhoneNumber" class="form-control" />
                <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span>
            </div>
            <hr />
            <div class="form-group">
                <label asp-for="Input.Password"></label>
                <input asp-for="Input.Password" class="form-control" />
                <span asp-validation-for="Input.Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Input.ConfirmPassword"></label>
                <input asp-for="Input.ConfirmPassword" class="form-control" />
                <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span>
            </div>

            <button type="submit" class="btn btn-primary">Register</button>
        </form>

Now if you try to run the project, you will get the following error: InvalidOperationException: Unable to resolve service for type 'Microsoft.AspNetCore.Identity.UI.Services.IEmailSender' while attempting to activate 'AspNetCoreCustomIdentyJwtDemo.Areas.Identity.Pages.Account.RegisterModel'.

This error is caused because the registration page sends an email confirmation, and an email provider has to be provided through dependency injection in the startup. There are a couple of ways of doing this. If you have access to an SMTP server, you can use the MailKit NuGet package. If you don't have access to an SMTP server, you can use a service like SendGrid. SendGrid has a free version that works for small projects. Once you get to a larger volume of email, there is a charge, but it is pretty reasonable if you don't want to build your own email infrastructure.

Since this is just a demo, we aren't going to configure an email service. Instead, we will just create a stub.

Now from the IDE, right-click the project and select Add then select *New Folder and name the folder services

Next right-click the services folder and select Add, then select Class. Name the class EmailSender.

Now replace the code between the namespace declaration in the EmailSender.cs file with the following.

public interface IEmailSender
{
    Task SendEmailAsync(string email, string subject, string message);
}

public class EmailSender : IEmailSender
{
    public Task SendEmailAsync(string email, string subject, string message)
    {
        return Task.CompletedTask;
    }
} 

Now open your startup.cs file in the root of your project and add the following line at the bottom of the ConfigureServices method.

services.AddSingleton<IEmailSender, EmailSender>();

Next you will need to Edit the Areas\Identity\Pages\Account\Manage\Register.cshtml.cs file again and remove the following using statement.

using Microsoft.AspNetCore.Identity.UI.Services;

You will want to add the using statement for your services directory.

using AspNetCoreCustomIdentyJwtDemo.services;

Now you should be able to build and run the solution. You can test registering and logging in. With this solution running, we are ready to add the JWT Authentication to our project.

JWT Authentication

The first thing we want to do is create an API Controller to test with. Go to your terminal window and make sure you are in the root of the project and type the following command.

dotnet aspnet-codegenerator controller -name Values -api -outDir Controllers/Api

Open the Controllers/api/ValuesController.cs files and replace the code with the following.

ValuesController.cs

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

namespace AspNetCoreCustomIdentyJwtDemo.Controllers_Api
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public List<string> GetValues()
        {
            return new List<string>() { "Value1", "Value2" };
        }
    }
}

Since this doesn't have any authentication configured yet, you can test this in a standard browser and you should receive the list of values on the page.

Next, you will want to add the [Authorize] attribute to the controller and test that you are redirected to the identity login. Your controller should look like this.

[Route("api/[controller]")]
    [Authorize]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public List<string> GetValues()
        {
            return new List<string>() { "Value1", "Value2" };
        }
    }

The next step is to add the System.IdentityModel.Tokens.Jwt to your project from NuGet. Type the following command to add the package.

dotnet add package System.IdentityModel.Tokens.Jwt

now we need to add the Microsoft.AspNetCore.Authentication.JwtBearer package. Type the following command to add the package.

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Now open the appsettings.json in the root of the project and add the following configuration.

"jwtTokenConfig": {
    "secret": "1234567890123456789",
    "issuer": "Issuer Name",
    "audience": "ApiUser",
    "accessTokenExpiration": 20,
    "refreshTokenExpiration": 60
  },
  • The secret needs to be at least 13 characters long and can be numbers, letters, and special characters. The issuer can be the name of the application or a company name.
  • The audience should be a single word that describes the users and will be a part of the claim. You can create multiple configs to support different audiences.

Next, right-click on the models folder and select add and then select class and name it JwtTokenConfig and place the following code.

JwtTokenConfig.cs

 using Microsoft.AspNetCore.Authentication.JwtBearer;
using System.Text.Json.Serialization;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class JwtTokenConfig
    {
        [JsonPropertyName("secret")]
        public string Secret { get; set; }

        [JsonPropertyName("issuer")]
        public string Issuer { get; set; }

        [JsonPropertyName("audience")]
        public string Audience { get; set; }

        [JsonPropertyName("accessTokenExpiration")]
        public int AccessTokenExpiration { get; set; }

        [JsonPropertyName("refreshTokenExpiration")]
        public int RefreshTokenExpiration { get; set; }

        public const string AuthSchemes = "Identity.Application" + "," + JwtBearerDefaults.AuthenticationScheme;
    }
}

We will use this class to hold the configuration from the appsettings.json file.


Next, right-click on the models folder and select add and then select class and name it RefreshToken and place the following code.

RefreshToken.cs

using System;
using System.Text.Json.Serialization;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class RefreshToken
    {
        [JsonPropertyName("username")]
        public string UserName { get; set; }    // can be used for usage tracking

        // can optionally include other metadata, such as user agent, IP address, device name, and so on

        [JsonPropertyName("tokenString")]
        public string TokenString { get; set; }

        [JsonPropertyName("expireAt")]
        public DateTime ExpireAt { get; set; }
    }
}

Next, right-click on the models folder and select add and then select class and name it JwtAuthResult and place the following.

JwtAuthResult.cs

using System.Text.Json.Serialization;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class JwtAuthResult
    {
        [JsonPropertyName("accessToken")]
        public string AccessToken { get; set; }

        [JsonPropertyName("refreshToken")]
        public RefreshToken RefreshToken { get; set; }
    }
}

This will hold the tokens generated by our JwtAuthService that we will be creating.


Next, right-click on the model and select Add then select Class and name the class file LoginRequest. Place the following code.

LoginRequest.cs

using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class LoginRequest
    {
        public LoginRequest(string userName, string password)
        {
            UserName = userName;
            Password = password;
        }

        public LoginRequest()
        {
        }

        [Required]
        [JsonPropertyName("username")]
        public string UserName { get; set; }

        [Required]
        [JsonPropertyName("password")]
        public string Password { get; set; }
    }
}

This class will be used to map the json retrieved by the client to a LoginRequest object.


Next, right-click on the model and select Add then select Class and name the class file LoginResult. Place the following code.

LoginResult.cs

using System.Text.Json.Serialization;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class LoginResult
    {
        [JsonPropertyName("username")]
        public string UserName { get; set; }

        [JsonPropertyName("role")]
        public string[] Roles { get; set; }

        [JsonPropertyName("originalUserName")]
        public string OriginalUserName { get; set; }

        [JsonPropertyName("accessToken")]
        public string AccessToken { get; set; }

        [JsonPropertyName("refreshToken")]
        public string RefreshToken { get; set; }
    }
}

Next, right-click on the model and select Add then select Class and name the class file Registration. Place the following code.

Registration.cs

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class Registration
    {
        public string UserName { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        public string PhoneNumber { get; set; }

        public string Email { get; set; }

        public string Password { get; set; }
    }
}

Next, right-click on the model and select Add then select Class and name the class file RefreshTokenRequest. Place the following code in the file.

RefreshTokenRequest.cs

using System.Text.Json.Serialization;

namespace AspNetCoreCustomIdentyJwtDemo.Models
{
    public class RefreshTokenRequest
    {
        [JsonPropertyName("refreshToken")]
        public string RefreshToken { get; set; }
    }
}

Now right click on the services folder and select Add and the select Class and name the class JwtAuthService. Replace the code in the file with the following.

JwtAuthService.cs

using AspNetCoreCustomIdentyJwtDemo.Models;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;

namespace AspNetCoreCustomIdentyJwtDemo.services
{
    public interface IJwtAuthService
    {
        IImmutableDictionary<string, RefreshToken> UsersRefreshTokensReadOnlyDictionary { get; }

        JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now);

        JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now);

        void RemoveExpiredRefreshTokens(DateTime now);

        void RemoveRefreshTokenByUserName(string userName);

        (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token);
    }

    public class JwtAuthService : IJwtAuthService
    {
        public IImmutableDictionary<string, RefreshToken> UsersRefreshTokensReadOnlyDictionary => _usersRefreshTokens.ToImmutableDictionary();
        private readonly ConcurrentDictionary<string, RefreshToken> _usersRefreshTokens;  // can store in a database or a distributed cache
        private readonly JwtTokenConfig _jwtTokenConfig;
        private readonly byte[] _secret;

        public JwtAuthService(JwtTokenConfig jwtTokenConfig)
        {
            _jwtTokenConfig = jwtTokenConfig;
            _usersRefreshTokens = new ConcurrentDictionary<string, RefreshToken>();
            _secret = Encoding.ASCII.GetBytes(jwtTokenConfig.Secret);
        }

        // optional: clean up expired refresh tokens
        public void RemoveExpiredRefreshTokens(DateTime now)
        {
            var expiredTokens = _usersRefreshTokens.Where(x => x.Value.ExpireAt < now).ToList();
            foreach (var expiredToken in expiredTokens)
            {
                _usersRefreshTokens.TryRemove(expiredToken.Key, out _);
            }
        }

        // can be more specific to ip, user agent, device name, etc.
        public void RemoveRefreshTokenByUserName(string userName)
        {
            var refreshTokens = _usersRefreshTokens.Where(x => x.Value.UserName == userName).ToList();
            foreach (var refreshToken in refreshTokens)
            {
                _usersRefreshTokens.TryRemove(refreshToken.Key, out _);
            }
        }

        public JwtAuthResult GenerateTokens(string username, Claim[] claims, DateTime now)
        {
            var shouldAddAudienceClaim = string.IsNullOrWhiteSpace(claims?.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Aud)?.Value);
            var jwtToken = new JwtSecurityToken(
                _jwtTokenConfig.Issuer,
                shouldAddAudienceClaim ? _jwtTokenConfig.Audience : string.Empty,
                claims,
                expires: now.AddMinutes(_jwtTokenConfig.AccessTokenExpiration),
                signingCredentials: new SigningCredentials(new SymmetricSecurityKey(_secret), SecurityAlgorithms.HmacSha256Signature));
            var accessToken = new JwtSecurityTokenHandler().WriteToken(jwtToken);

            var refreshToken = new RefreshToken
            {
                UserName = username,
                TokenString = GenerateRefreshTokenString(),
                ExpireAt = now.AddMinutes(_jwtTokenConfig.RefreshTokenExpiration)
            };
            _usersRefreshTokens.AddOrUpdate(refreshToken.TokenString, refreshToken, (s, t) => refreshToken);

            return new JwtAuthResult
            {
                AccessToken = accessToken,
                RefreshToken = refreshToken
            };
        }

        public JwtAuthResult Refresh(string refreshToken, string accessToken, DateTime now)
        {
            var (principal, jwtToken) = DecodeJwtToken(accessToken);
            if (jwtToken == null || !jwtToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256Signature))
            {
                throw new SecurityTokenException("Invalid token");
            }

            var userName = principal.Identity.Name;
            if (!_usersRefreshTokens.TryGetValue(refreshToken, out var existingRefreshToken))
            {
                throw new SecurityTokenException("Invalid token");
            }
            if (existingRefreshToken.UserName != userName || existingRefreshToken.ExpireAt < now)
            {
                throw new SecurityTokenException("Invalid token");
            }

            return GenerateTokens(userName, principal.Claims.ToArray(), now); // need to recover the original claims
        }

        public (ClaimsPrincipal, JwtSecurityToken) DecodeJwtToken(string token)
        {
            if (string.IsNullOrWhiteSpace(token))
            {
                throw new SecurityTokenException("Invalid token");
            }
            var principal = new JwtSecurityTokenHandler()
                .ValidateToken(token,
                    new TokenValidationParameters
                    {
                        ValidateIssuer = true,
                        ValidIssuer = _jwtTokenConfig.Issuer,
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(_secret),
                        ValidAudience = _jwtTokenConfig.Audience,
                        ValidateAudience = true,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.FromMinutes(1)
                    },
                    out var validatedToken);
            return (principal, validatedToken as JwtSecurityToken);
        }

        private static string GenerateRefreshTokenString()
        {
            var randomNumber = new byte[32];
            using var randomNumberGenerator = RandomNumberGenerator.Create();
            randomNumberGenerator.GetBytes(randomNumber);
            return Convert.ToBase64String(randomNumber);
        }
    }
}

This is where we generate the token and build the claims that get sent to the client.


Now right click on the services folder and select Add and the select Class and name the class AccountService. Replace the code in the file with the following.

AccountService.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNetCoreCustomIdentyJwtDemo.Models;
using AspNetCoreCustomIdentyJwtDemo.services;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;

namespace AspNetCoreCustomIdentityJwtDemo.services
{
    public interface IAccountService
    {
        Task<bool> IsValidUserCredentials(LoginRequest loginRequest);

        Task<LoginResult> Login(LoginRequest loginRequest);

        Task<IdentityResult> Register(Registration registration);

        void Logout(string? userName);

        JwtAuthResult Refresh(string requestRefreshToken, string accessToken, in DateTime now);
    }

    public class AccountService : IAccountService
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly IJwtAuthService _jwtAuthService;
        private readonly ILogger<AccountService> _logger;

        public AccountService(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, IJwtAuthService jwtAuthService, ILogger<AccountService> logger)
        {
            _signInManager = signInManager;
            _userManager = userManager;
            _jwtAuthService = jwtAuthService;
            _logger = logger;
        }

        public async Task<bool> IsValidUserCredentials(LoginRequest loginRequest)
        {
            var user = await _userManager.FindByNameAsync(loginRequest.UserName);

            var signInResult = await _signInManager.CheckPasswordSignInAsync(user, loginRequest.Password, false);

            return signInResult.Succeeded;
        }

        public async Task<LoginResult> Login(LoginRequest loginRequest)
        {
            var user = await _userManager.FindByNameAsync(loginRequest.UserName);
            
            var roles = await _userManager.GetRolesAsync(user);

            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, loginRequest.UserName)
            };

            if (roles != null)
                foreach (var role in roles)
                {
                    claims.Add(new Claim(ClaimTypes.Role, role));
                }

            
            var jwtResult = _jwtAuthService.GenerateTokens(loginRequest.UserName, claims.ToArray(), DateTime.Now);
            _logger.LogInformation($"User [{loginRequest.UserName}] logged in the system.");
            return new LoginResult
            {
                UserName = loginRequest.UserName,
                Roles = roles?.ToArray(),
                AccessToken = jwtResult.AccessToken,
                RefreshToken = jwtResult.RefreshToken.TokenString
            };
        }

        public async Task<IdentityResult> Register(Registration registration)
        {
            var user = new ApplicationUser
            {
                UserName = registration.UserName,
                Email = registration.Email,
                FirstName = registration.FirstName,
                LastName = registration.LastName,
                PhoneNumber = registration.PhoneNumber,
                EmailConfirmed = true
            };
            var result = await _userManager.CreateAsync(user, registration.Password);

            return result;
        }

        public void Logout(string? userName)
        {
            _jwtAuthService.RemoveRefreshTokenByUserName(userName);
            _logger.LogInformation($"User [{userName}] logged out the system.");
        }

        public JwtAuthResult Refresh(string requestRefreshToken, string accessToken, in DateTime now)
        {
            return _jwtAuthService.Refresh(requestRefreshToken, accessToken, DateTime.Now);
        }
    }
}

The account service is what backs the account controller. The code in this service is pulled from the identity razor pages for the login and registration, then we access the JwtAuthService for generating the access token and the refresh token.

Next, we will create the account controller. Use your terminal window at the root of your project and type the following command.

dotnet aspnet-codegenerator controller -name Account -api -outDir Controllers/Api

Now open the controllers/api/AccountController.cs file and replace the contents with the following.

AccountController.cs

using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNetCoreCustomIdentityJwtDemo.services;
using AspNetCoreCustomIdentyJwtDemo.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;

namespace AspNetCoreCustomIdentityJwtDemo.Controllers.Api
{
    [Route("api/[controller]")]
    [ApiController]
    [EnableCors("AllowOrigin")]
    public class AccountController : ControllerBase
    {
        private readonly SignInManager<ApplicationUser> _signInManager;
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly IAccountService _accountService;
        private readonly ILogger<AccountController> _logger;

        public AccountController(IAccountService accountService, SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager, ILogger<AccountController> logger)
        {
            _signInManager = signInManager;
            _userManager = userManager;
            _accountService = accountService;
            _logger = logger;
        }

        
        [AllowAnonymous]
        [HttpPost("login")]
        public async Task<IActionResult> Login([FromBody] LoginRequest request)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest();
            }

            if (!await _accountService.IsValidUserCredentials(request))
            {
                return Unauthorized();
            }

            return Ok(await _accountService.Login(request));
        }

        [AllowAnonymous]
        [HttpPost("register")]
        public async Task<IActionResult> Register([FromBody] Registration registration)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest();
            }

            var result = await _accountService.Register(registration);

            if (result.Succeeded)
            {
                _logger.LogInformation("User created a new account with password.");

                var request = new LoginRequest(registration.UserName, registration.Password);
                return Ok(await _accountService.Login(request));
            }

            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }

            return BadRequest();
        }

        [HttpGet("user")]
        [Authorize(AuthenticationSchemes = JwtTokenConfig.AuthSchemes)]
        public ActionResult GetCurrentUser()
        {
            return Ok(new LoginResult
            {
                UserName = User.Identity.Name,
                Roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray(),
                OriginalUserName = User.FindFirst("OriginalUserName")?.Value
            });
        }

        [HttpPost("logout")]
        [Authorize(AuthenticationSchemes = JwtTokenConfig.AuthSchemes)]
        public ActionResult Logout()
        {
            var userName = User.Identity.Name;
            _accountService.Logout(userName);
            return Ok();
        }

        [HttpPost("refresh-token")]
        [Authorize(AuthenticationSchemes = JwtTokenConfig.AuthSchemes)]
        public async Task<ActionResult> RefreshToken([FromBody] RefreshTokenRequest request)
        {
            try
            {
                var userName = User.Identity.Name;
                _logger.LogInformation($"User [{userName}] is trying to refresh JWT token.");

                if (string.IsNullOrWhiteSpace(request.RefreshToken))
                {
                    return Unauthorized();
                }

                var accessToken = await HttpContext.GetTokenAsync("Bearer", "access_token");

                
                var jwtResult = _accountService.Refresh(request.RefreshToken, accessToken, DateTime.Now);
                _logger.LogInformation($"User [{userName}] has refreshed JWT token.");
                return Ok(new LoginResult
                {
                    UserName = userName,
                    Roles = User.FindAll(ClaimTypes.Role).Select(c => c.Value).ToArray(),
                    AccessToken = jwtResult.AccessToken,
                    RefreshToken = jwtResult.RefreshToken.TokenString
                });
            }
            catch (SecurityTokenException e)
            {
                return Unauthorized(e.Message); // return 401 so that the client side can redirect the user to login page
            }
        }
    }
}

Now we need to wire everything together in the startup.cs file located in the root of our project. The first thing we need to do is retrieve our JWT configuration values from the appsettings.json file. NOTE: In a production environment, you would want to choose a more secure location than the appsettings.json file, but for demo purposes, this is an easy way to set things up.

Open the startup.cs file and add the following lines at the top of the ConfigureServices method.

var jwtTokenConfig = Configuration.GetSection("jwtTokenConfig").Get<JwtTokenConfig>();
services.AddSingleton(jwtTokenConfig);

Now we need to add the cookie configuration for our normal identity, so we can route users to the login area. Right below the services.AddIdentity, add the following lines.

services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly = true;
                options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

                options.LoginPath = "/Identity/Account/Login";
                options.AccessDeniedPath = "/Identity/Account/AccessDenied";
                options.SlidingExpiration = true;
            });

Next, we want to add the JWT authentication right below the cookie configuration we just added. Insert the following lines to wire up the JWT token authentication.

services.AddAuthentication()
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = true;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidIssuer = jwtTokenConfig.Issuer,
                        ValidateAudience = true,
                        ValidAudience = jwtTokenConfig.Audience,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.FromMinutes(1),
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenConfig.Secret))
                    };
                });

Now we need to add CORS, so our angular and mobile clients can access the APIs. Add the following lines right below the JWT configuration.

services.AddCors(c =>
            {
                c.AddPolicy("AllowOrigin", options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
            });

Note: These parameters open CORS all the way. In a production application, you would want to lock these down to only the functionality you want to be made available to your clients.

Next, we need to add our service for dependency injection. Add the following lines to the bottom of the end of the ConfigureServices method.

services.AddScoped<IAccountService, AccountService>();
services.AddSingleton<IJwtAuthService, JwtAuthService>();

In the **configure **method you will want to add the following line right below the app.UseRouting.

app.UseCors();

With all the edits in place, your startup.cs file should look like this.

startup.cs

using System;
using System.Text;
using AspNetCoreCustomIdentityJwtDemo.services;
using AspNetCoreCustomIdentyJwtDemo.Data;
using AspNetCoreCustomIdentyJwtDemo.Models;
using AspNetCoreCustomIdentyJwtDemo.services;
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.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace AspNetCoreCustomIdentyJwtDemo
{
    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)
        {
            var jwtTokenConfig = Configuration.GetSection("jwtTokenConfig").Get<JwtTokenConfig>();
            services.AddSingleton(jwtTokenConfig);

            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, ApplicationRole>(
                    options =>
                    {
                        options.SignIn.RequireConfirmedEmail = true;
                        options.Stores.MaxLengthForKeys = 128;
                    })
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly = true;
                options.ExpireTimeSpan = TimeSpan.FromMinutes(5);

                options.LoginPath = "/Identity/Account/Login";
                options.AccessDeniedPath = "/Identity/Account/AccessDenied";
                options.SlidingExpiration = true;
            });

            services.AddAuthentication()
                .AddJwtBearer(cfg =>
                {
                    cfg.RequireHttpsMetadata = true;
                    cfg.SaveToken = true;
                    cfg.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters()
                    {
                        ValidateIssuer = true,
                        ValidIssuer = jwtTokenConfig.Issuer,
                        ValidateAudience = true,
                        ValidAudience = jwtTokenConfig.Audience,
                        ValidateLifetime = true,
                        ClockSkew = TimeSpan.FromMinutes(1),
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtTokenConfig.Secret))
                    };
                });

            services.AddCors(c =>
            {
                c.AddPolicy("AllowOrigin", options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
            });


            services.AddControllersWithViews();
            services.AddRazorPages();

            services.AddSingleton<IEmailSender, EmailSender>();
            services.AddScoped<IAccountService, AccountService>();
            services.AddSingleton<IJwtAuthService, JwtAuthService>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app,
            IWebHostEnvironment env, ApplicationDbContext context,
            RoleManager<ApplicationRole> roleManager,
            UserManager<ApplicationUser> userManager)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }
            app.UseHttpsRedirection();
            app.UseStaticFiles();

            app.UseRouting();
            app.UseCors();

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllerRoute(
                    name: "default",
                    pattern: "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapRazorPages();
            });

            SeedData.Initialize(context, userManager, roleManager).Wait();
        }
    }
}

With that, the JWT authentication should be working. If you create an ID by registering, you will need to edit the record in the AspNetUsers table and set the EmailConfirmed to True. You can test this controller using Postman. First, you will need to add a header

KEY : Content-Type
VALUE : application/json

Next, you will need to add a JSON object to the body that contains the UserName and Password. For this to work you should have already created a user with the web-based Identity registration. Select the Raw tab in Postman and be sure to change the drop-down from Text to JSON.

{
    "UserName":"TestUser",
    "Password":"Password123!"
}

If everything is working, you should receive a JSON object back that includes an Access token.

The code for this project is located in my GitHub repository here

My next walkthrough takes this code for the backend and I generate an Angular client that can register and log in using the APIs we just created. Check out that walkthrough here

Comments

Popular posts from this blog

File Backups to Dropbox with PowerShell

Dynamic Expression Builder with EF Core