[ASP.NET Core][Entity Framework Core] Try JWT 1
Tags: csharp, entityframeworkcore, aspnetcore
Published: 2022-03-12 17:01:55

## Intro
This time, I will try ASP.NET Core authentication with JWT(JSON Web Token).
I will just try it first.
And next time I would like to see the detailed it.

This sample project is based on the project what I created last time.

* [[ASP.NET Core][Entity Framework Core] Try System.Text.Json](https://dev.to/masanori_msl/aspnet-coreentity-framework-core-try-systemtextjson-54bn)

And I added some codes from another project what I tried ASP.NET Core Identity last time.

* [【.NET 5】【ASP.NET Core Identity】SignIn with custom user](https://dev.to/masanori_msl/net-5-asp-net-core-identity-signin-with-custom-user-56fe)

### Environments
* .NET ver.6.0.201
* Microsoft.EntityFrameworkCore ver.6.0.3
* Microsoft.EntityFrameworkCore.Design ver.6.0.3
* Npgsql.EntityFrameworkCore.PostgreSQL ver.6.0.3
* NLog.Web.AspNetCore ver.4.14.0
* Microsoft.AspNetCore.Identity.EntityFrameworkCore version="6.0.3
* Microsoft.AspNetCore.Authentication.JwtBearer ver.6.0.3

## Base project
#### Program.cs
```cs
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;
using NLog.Web;
using System.Net;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using BookshelfSample.Books;
using BookshelfSample.Models;
using BookshelfSample.Users;
using BookshelfSample.Users.Repositories;

var logger = NLogBuilder.ConfigureNLog("Nlog.config").GetCurrentClassLogger();
try
{
    var builder = WebApplication.CreateBuilder(args);
    builder.Host.ConfigureLogging(logging =>
    {
        logging.ClearProviders();
        logging.AddConsole();
    })
    .UseNLog();
    builder.Services.AddRazorPages();
    builder.Services.AddControllers()
        .AddJsonOptions(options =>
        {
            options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
        });
    builder.Services.AddDbContext<BookshelfContext>(options =>
    {
        options.EnableSensitiveDataLogging();
        options.UseNpgsql(builder.Configuration["DbConnection"]);
    });
    // ApplicationUser.cs, ApplicationUserStore.cs are as same as last time.
    // https://dev.to/masanori_msl/net-5-asp-net-core-identity-signin-with-custom-user-56fe
    builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                .AddUserStore<ApplicationUserStore>()
                .AddEntityFrameworkStores<BookshelfContext>()
                .AddDefaultTokenProviders();
...
    builder.Services.AddScoped<IApplicationUsers, ApplicationUsers>();
    builder.Services.AddScoped<IApplicationUserService, ApplicationUserService>();

    var app = builder.Build();
    app.UseStaticFiles();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    app.Run();
}
catch (Exception ex)
{
    string type = ex.GetType().Name;
    if (type.Equals("StopTheHostException", StringComparison.Ordinal))
    {
        throw;
    }
    logger.Error(ex, "Stopped program because of exception");
}
finally
{
    NLog.LogManager.Shutdown();
}
```
#### PageController.cs
```cs
using BookshelfSample.Users;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace BookshelfSample.Controllers;

[Authorize]
public class PageController: Controller
{
    private readonly ILogger<PageController> logger;
    public PageController(ILogger<PageController> logger)
    {
        this.logger = logger;
    }
    [Route("/")]
    [Route("/pages")]
    [Route("/pages/index")]
    public IActionResult Index()
    {
        return View("Views/Index.cshtml");
    }
    [AllowAnonymous]
    [Route("/pages/signin")]
    public IActionResult Signin()
    {
        return View("Views/Signin.cshtml");
    }
}
```
#### UserController.cs
```cs
using BookshelfSample.Apps;
using BookshelfSample.Users;
using BookshelfSample.Users.Dto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace BookshelfSample.Controllers;

[Authorize]
public class UserController: Controller
{
    private readonly IApplicationUserService userService;
    public UserController(IApplicationUserService userService)
    {
        this.userService = userService;
    }
    [AllowAnonymous]
    [HttpPost]
    [Route("/user/signin")]
    public async Task<UserActionResult> Signin([FromBody] SigninValue? value)
    {
        return await this.userService.SigninAsync(value, HttpContext.Session);
    }
}
```
#### ApplicationUserService.cs
```cs
using BookshelfSample.Apps;
using BookshelfSample.Users.Dto;
using BookshelfSample.Users.Repositories;
using Microsoft.AspNetCore.Identity;
namespace BookshelfSample.Users;

public class ApplicationUserService: IApplicationUserService
{
    private readonly SignInManager<ApplicationUser> signInManager;
    private readonly IApplicationUsers users;
    private readonly IUserTokens userTokens;

    public ApplicationUserService(SignInManager<ApplicationUser> signInManager,
        IApplicationUsers users,
        IUserTokens userTokens)
    {
        this.signInManager = signInManager;
        this.users = users;
        this.userTokens = userTokens;
    }
    public async Task<UserActionResult> SigninAsync(SigninValue value, ISession session)
    {
        var target = await this.users.GetByEmailForSigninAsync(value.Email);
        if(target == null)
        {
            return ActionResultFactory.GetFailed("Invalid e-mail or password");
        }
        var result = await this.signInManager.PasswordSignInAsync(target, value.Password, false, false);
        if(result.Succeeded)
        {
            return ActionResultFactory.GetSucceeded();
        }
        return ActionResultFactory.GetFailed("Invalid e-mail or password");
    }
}
```
#### ApplicationUsers.cs
```cs
using BookshelfSample.Models;
using Microsoft.EntityFrameworkCore;
namespace BookshelfSample.Users.Repositories;

public class ApplicationUsers: IApplicationUsers
{
    private readonly BookshelfContext context;

    public ApplicationUsers(BookshelfContext context)
    {
        this.context = context;
    }
    public async Task<ApplicationUser?> GetByEmailForSigninAsync(string email)
    {
        return await this.context.ApplicationUsers
            .AsNoTracking()
            .FirstOrDefaultAsync(u => u.Email == email);
    }
}
```
#### SigninValue.cs
```cs
namespace BookshelfSample.Users.Dto;
public record SigninValue(string Email, string Password);
```

## Add JWT
To authenticate with JWT, I have to add "Microsoft.AspNetCore.Authentication.JwtBearer" and put it into Program.cs.

#### Program.cs
```cs
...
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = builder.Configuration["Jwt:Issuer"],
                ValidAudience = builder.Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
            };
        });
...
```
#### appsettings.json
```json
{
...
  "Jwt": {
    "Issuer": "http://localhost:5110",
    "Audience": "http://localhost:5110",
    "Key": "1234567890abcdefg"
  }
}
```

#### Authorize attribute
By default, the Authorize attribute works for cookie based authentication.
To use JWT, I have to add it.

#### UserTokens.cs
```cs
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;

namespace BookshelfSample.Users;

public class UserTokens: IUserTokens
{
    private readonly IConfiguration config;
    public UserTokens(ILogger<UserTokens> logger,
        IConfiguration config)
    {
        this.config = config;
    }
    public const string AuthSchemes = JwtBearerDefaults.AuthenticationScheme;
    // When I also use cookie based authentication, I will uncomment below.
    //  + "," + CookieAuthenticationDefaults.AuthenticationScheme;

    // After signing in, I generate a token and set it into request headers.
    public string GenerateToken(ApplicationUser user)
    {
        return new JwtSecurityTokenHandler()
            .WriteToken(new JwtSecurityToken(this.config["Jwt:Issuer"],
                this.config["Jwt:Audience"],
                claims: new []
                {
                    new Claim(ClaimTypes.Email, user.Email)
                },
                expires: DateTime.Now.AddMinutes(30),
                signingCredentials: new SigningCredentials(
                    new SymmetricSecurityKey(Encoding.UTF8.GetBytes(this.config["Jwt:Key"])),
                    SecurityAlgorithms.HmacSha256)));
    }
}
```
#### PageController.cs
```cs
...
[Authorize(AuthenticationSchemes = UserTokens.AuthSchemes)]
public class PageController: Controller
{
...
    // this page is only for signed in user.
    [Route("/")]
    [Route("/pages")]
    [Route("/pages/index")]
    public IActionResult Index()
    {
        return View("Views/Index.cshtml");
    }
    // everyone can open this page.
    [AllowAnonymous]
    [Route("/pages/signin")]
    public IActionResult Signin()
    {
        return View("Views/Signin.cshtml");
    }
}
```

## Save tokens
After siginin in, I generate a token by "GenerateToken" and set it into request headers.
In this time, I save it by session and I will get it every access.

#### ApplicationUserService.cs
```cs
...
public class ApplicationUserService: IApplicationUserService
{
...
    public async Task<UserActionResult> SigninAsync(SigninValue value, ISession session)
    {
        var target = await this.users.GetByEmailForSigninAsync(value.Email);
        if(target == null)
        {
            return ActionResultFactory.GetFailed("Invalid e-mail or password");
        }
        var result = await this.signInManager.PasswordSignInAsync(target, value.Password, false, false);
        if(result.Succeeded)
        {
            // Generate a token and set it into session.
            var token = this.userTokens.GenerateToken(target);
            session.SetString("user-token", token);
            return ActionResultFactory.GetSucceeded();
        }
        return ActionResultFactory.GetFailed("Invalid e-mail or password");
    }
...
}
```
#### Program.cs
```cs
...
    builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,
                ValidIssuer = builder.Configuration["Jwt:Issuer"],
                ValidAudience = builder.Configuration["Jwt:Audience"],
                IssuerSigningKey = new SymmetricSecurityKey(
                    Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Key"])),
            };
        });
    builder.Services.AddSession(options => {
        options.IdleTimeout = TimeSpan.FromSeconds(30);
        options.Cookie.HttpOnly = true;
        options.Cookie.IsEssential = true;
        options.Cookie.SameSite = SameSiteMode.Strict;
    });
...
    builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>()
                .AddUserStore<ApplicationUserStore>()
                .AddEntityFrameworkStores<BookshelfContext>()
                .AddDefaultTokenProviders();
...
    app.UseStaticFiles();

    // I must set the token before "app.UseAuthentication".
    // If I execute it first, the request return 401 error.
    app.UseSession();
    app.Use(async (context, next) =>
    {
        var token = context.Session.GetString("user-token");
        if(string.IsNullOrEmpty(token) == false)
        {            
            context.Request.Headers.Add("Authorization", $"Bearer {token}");
        }
        await next();
    });
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    app.Run();
...
```
## Auto redirection for 401
When I use cookie based authentications, I can use events of ApplicationCookie to redirect to the sign in page like below.

* [Return 401 Unauthorized from ASP.NET Core API - Michal Dymel - DevBlog](https://devblog.dymel.pl/2016/07/07/return-401-unauthorized-from-asp-net-core-api/)

But because I use JWT in this time, so it doesn't work.
Thus I use "UseStatusCodePages".

#### Program.cs
```cs
...
    var app = builder.Build();
    app.UseStaticFiles();
    app.UseSession();
    app.Use(async (context, next) =>
    {
        var token = context.Session.GetString("user-token");
        if(string.IsNullOrEmpty(token) == false)
        {            
            context.Request.Headers.Add("Authorization", $"Bearer {token}");
        }
        await next();
    });
    // This also must be execute before "app.UseAuthentication".
    app.UseStatusCodePages(async context =>
    {
        if (context.HttpContext.Response.StatusCode == (int)HttpStatusCode.Unauthorized)
        {
            // redirect only for Razor pages.
            if(context.HttpContext.Request.Path.StartsWithSegments("/pages"))
            {
                context.HttpContext.Response.Redirect("/pages/signin");
            }
            else
            {
                context.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
            }            
            return;
        }
        await context.Next(context.HttpContext);
    });
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
    app.Run();
...
```

## Resources
* [RFC7519 - JSON Web Token (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
* [Overview of ASP.NET Core authentication - Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-6.0)
* [Authorize with a specific scheme in ASP.NET Core - Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-6.0)
* [Implementing JWT Authentication in ASP.NET Core 5 - CODE Magazine](https://codemag.com/Article/2105051/Implementing-JWT-Authentication-in-ASP.NET-Core-5)
* [JWT Validation and Authorization in ASP.NET Core - .NET Blog](https://devblogs.microsoft.com/dotnet/jwt-validation-and-authorization-in-asp-net-core/)
* [Session and state management in ASP.NET Core - Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/app-state?view=aspnetcore-6.0)
* [Handle errors in ASP.NET Core - Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-6.0)
* [How to redirect to log in page on 401 using JWT authorization in ASP.NET Core - StackOverflow](https://stackoverflow.com/questions/46230596/how-to-redirect-to-log-in-page-on-401-using-jwt-authorization-in-asp-net-core)