- .NET 5.0 – JWT Authentication Tutorial with Example API
- .NET 5.0 API – JWT Authentication with Refresh Tokens
- ASP.NET Core 6 how to access Configuration during setup
- JWT Token Expiration time failing .net core [duplicate]
สร้างโปรเจ็กส์แบบ ASP.NET Core Web API
ติดตั้ง Package Microsoft.IdentityModel.Tokens 6.15.0
PM> Install-Package Microsoft.IdentityModel.Tokens -Version 6.15.0
ติดตั้ง Package System.IdentityModel.Tokens.Jwt 6.15.0
PM> Install-Package System.IdentityModel.Tokens.Jwt -Version 6.15.0
สร้างคลาส Entities\User.cs
using System.Text.Json.Serialization; namespace WebApi1.Entities { public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Username { get; set; } [JsonIgnore] public string Password { get; set; } } }
สร้างคลาส Models\AuthenticateRequest.cs
using System.ComponentModel.DataAnnotations; namespace WebApi1.Models { public class AuthenticateRequest { [Required] public string Username { get; set; } [Required] public string Password { get; set; } } }
สร้างคลาส Models\AuthenticateResponse.cs
using WebApi1.Entities; namespace WebApi1.Models { public class AuthenticateResponse { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string Username { get; set; } public string Token { get; set; } public AuthenticateResponse(User user, string token) { Id = user.Id; FirstName = user.FirstName; LastName = user.LastName; Username = user.Username; Token = token; } } }
สร้างคลาส Helpers\AppSettings.cs
namespace WebApi1.Helpers { public class AppSettings { public string Secret { get; set; } } }
สร้างคลาส Helpers\AuthorizeAttribute.cs
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using WebApi1.Entities; [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] public class AuthorizeAttribute : Attribute, IAuthorizationFilter { public void OnAuthorization(AuthorizationFilterContext context) { var user = (User)context.HttpContext.Items["User"]; if (user == null) { // not logged in context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; } } }
สร้างคลาส Helpers\JwtMiddleware.cs
using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Text; using WebApi1.Services; namespace WebApi1.Helpers { public class JwtMiddleware { private readonly RequestDelegate _next; private readonly AppSettings _appSettings; public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings) { _next = next; _appSettings = appSettings.Value; } public async Task Invoke(HttpContext context, IUserService userService) { var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); if (token != null) attachUserToContext(context, userService, token); await _next(context); } private void attachUserToContext(HttpContext context, IUserService userService, string token) { try { var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_appSettings.Secret); tokenHandler.ValidateToken(token, new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(key), ValidateIssuer = false, ValidateAudience = false, // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) ClockSkew = TimeSpan.Zero }, out SecurityToken validatedToken); var jwtToken = (JwtSecurityToken)validatedToken; var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value); // attach user to context on successful jwt validation context.Items["User"] = userService.GetById(userId); } catch { // do nothing if jwt validation fails // user is not attached to context so request won't have access to secure routes } } } }
สร้างคลาส Services\UserService.cs
using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using WebApi1.Entities; using WebApi1.Helpers; using WebApi1.Models; namespace WebApi1.Services { public interface IUserService { AuthenticateResponse Authenticate(AuthenticateRequest model); IEnumerable<User> GetAll(); User GetById(int id); } public class UserService : IUserService { // users hardcoded for simplicity, store in a db with hashed passwords in production applications private List<User> _users = new List<User> { new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" } }; private readonly AppSettings _appSettings; public UserService(IOptions<AppSettings> appSettings) { _appSettings = appSettings.Value; } public AuthenticateResponse Authenticate(AuthenticateRequest model) { var user = _users.SingleOrDefault(x => x.Username == model.Username && x.Password == model.Password); // return null if user not found if (user == null) return null; // authentication successful so generate jwt token var token = generateJwtToken(user); return new AuthenticateResponse(user, token); } public IEnumerable<User> GetAll() { return _users; } public User GetById(int id) { return _users.FirstOrDefault(x => x.Id == id); } // helper methods private string generateJwtToken(User user) { // generate token that is valid for 7 days var tokenHandler = new JwtSecurityTokenHandler(); var key = Encoding.ASCII.GetBytes(_appSettings.Secret); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), //Expires = DateTime.UtcNow.AddDays(7), Expires = DateTime.UtcNow.AddMinutes(1), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); } } }
แก้ไขไฟล์ appsettings.json
{ "AppSettings": { "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" }
แก้ไขไฟล์ Program.cs
using WebApi1.Helpers; using WebApi1.Services; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle ConfigurationManager configuration = builder.Configuration; // configure strongly typed settings object builder.Services.Configure<AppSettings>(configuration.GetSection("AppSettings")); // configure DI for application services builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.UseMiddleware<JwtMiddleware>(); app.MapControllers(); app.Run();
สร้าง API Controller Controllers\UsersController.cs
using Microsoft.AspNetCore.Mvc; using WebApi1.Models; using WebApi1.Services; namespace WebApi1.Controllers { [Route("[controller]")] [ApiController] public class UsersController : ControllerBase { private IUserService _userService; public UsersController(IUserService userService) { _userService = userService; } [HttpPost("authenticate")] public IActionResult Authenticate(AuthenticateRequest model) { var response = _userService.Authenticate(model); if (response == null) return BadRequest(new { message = "Username or password is incorrect" }); return Ok(response); } [Authorize] [HttpGet] public IActionResult GetAll() { var users = _userService.GetAll(); return Ok(users); } } }
ทดลองเรียกใช้งาน https://localhost:7078/Users จะได้
{ "message": "Unauthorized" }
ต้องไปเรียก https://localhost:7078/Users/authenticate เพื่อขอ token มาก่อน
Request
{ "username": "test", "password": "test" }
Response
{ "id": 1, "firstName": "Test", "lastName": "User", "username": "test", "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEiLCJuYmYiOjE2Mzk1OTM0OTYsImV4cCI6MTYzOTU5MzU1NiwiaWF0IjoxNjM5NTkzNDk2fQ.ingwuhAl4TB7n7lOMILt-hWpN07ggjwQu4VJF6Lky2U" }
ถ้า username หรือ password ผิดจะได้
{ "message": "Username or password is incorrect" }
ทีนี้เรียก https://localhost:7078/Users พร้อมให้ค่า Bearer Token จะเรียกได้ละ (token มีอายุ 1 นาที ถ้าเกิน 1 นาทีก็จะ Unauthorized
)
[ { "id": 1, "firstName": "Test", "lastName": "User", "username": "test" } ]