DEV Community

Masui Masanori
Masui Masanori

Posted on

4 1

[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

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay