[ASP.NET Core][Entity Framework Core] Try JWT 1

masanori_msl

Masui Masanori

Posted on March 12, 2022

[ASP.NET Core][Entity Framework Core] Try JWT 1

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.

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

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

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();
}
Enter fullscreen mode Exit fullscreen mode

PageController.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");
    }
}
Enter fullscreen mode Exit fullscreen mode

UserController.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

ApplicationUserService.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");
    }
}
Enter fullscreen mode Exit fullscreen mode

ApplicationUsers.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);
    }
}
Enter fullscreen mode Exit fullscreen mode

SigninValue.cs

namespace BookshelfSample.Users.Dto;
public record SigninValue(string Email, string Password);
Enter fullscreen mode Exit fullscreen mode

Add JWT

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

Program.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"])),
            };
        });
...
Enter fullscreen mode Exit fullscreen mode

appsettings.json

{
...
  "Jwt": {
    "Issuer": "http://localhost:5110",
    "Audience": "http://localhost:5110",
    "Key": "1234567890abcdefg"
  }
}
Enter fullscreen mode Exit fullscreen mode

Authorize attribute

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

UserTokens.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)));
    }
}
Enter fullscreen mode Exit fullscreen mode

PageController.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");
    }
}
Enter fullscreen mode Exit fullscreen mode

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

...
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");
    }
...
}
Enter fullscreen mode Exit fullscreen mode

Program.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();
...
Enter fullscreen mode Exit fullscreen mode

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.

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

Program.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();
...
Enter fullscreen mode Exit fullscreen mode

Resources

💖 💪 🙅 🚩
masanori_msl
Masui Masanori

Posted on March 12, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related