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();
}
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");
}
}
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);
}
}
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");
}
}
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);
}
}
SigninValue.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
...
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
{
...
"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
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
...
[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
...
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
...
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.
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();
...
Resources
- RFC7519 - JSON Web Token (JWT)
- Overview of ASP.NET Core authentication - Microsoft Docs
- Authorize with a specific scheme in ASP.NET Core - Microsoft Docs
- Implementing JWT Authentication in ASP.NET Core 5 - CODE Magazine
- JWT Validation and Authorization in ASP.NET Core - .NET Blog
- Session and state management in ASP.NET Core - Microsoft Docs
- Handle errors in ASP.NET Core - Microsoft Docs
- How to redirect to log in page on 401 using JWT authorization in ASP.NET Core - StackOverflow
Top comments (0)