├── .gitignore ├── Controllers └── UsersController.cs ├── Entities └── User.cs ├── Helpers ├── AppSettings.cs ├── AuthorizeAttribute.cs └── JwtMiddleware.cs ├── LICENSE ├── Models ├── AuthenticateRequest.cs └── AuthenticateResponse.cs ├── Program.cs ├── README.md ├── Services └── UserService.cs ├── WebApi.csproj ├── appsettings.json └── omnisharp.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | typings 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # .NET compiled files 41 | bin 42 | obj -------------------------------------------------------------------------------- /Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Controllers; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using WebApi.Helpers; 5 | using WebApi.Models; 6 | using WebApi.Services; 7 | 8 | [ApiController] 9 | [Route("[controller]")] 10 | public class UsersController : ControllerBase 11 | { 12 | private IUserService _userService; 13 | 14 | public UsersController(IUserService userService) 15 | { 16 | _userService = userService; 17 | } 18 | 19 | [HttpPost("authenticate")] 20 | public IActionResult Authenticate(AuthenticateRequest model) 21 | { 22 | var response = _userService.Authenticate(model); 23 | 24 | if (response == null) 25 | return BadRequest(new { message = "Username or password is incorrect" }); 26 | 27 | return Ok(response); 28 | } 29 | 30 | [Authorize] 31 | [HttpGet] 32 | public IActionResult GetAll() 33 | { 34 | var users = _userService.GetAll(); 35 | return Ok(users); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Entities; 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | public class User 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public string Username { get; set; } 11 | 12 | [JsonIgnore] 13 | public string Password { get; set; } 14 | } -------------------------------------------------------------------------------- /Helpers/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | public class AppSettings 4 | { 5 | public string Secret { get; set; } 6 | } -------------------------------------------------------------------------------- /Helpers/AuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using WebApi.Entities; 6 | 7 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 8 | public class AuthorizeAttribute : Attribute, IAuthorizationFilter 9 | { 10 | public void OnAuthorization(AuthorizationFilterContext context) 11 | { 12 | var user = (User)context.HttpContext.Items["User"]; 13 | if (user == null) 14 | { 15 | // not logged in 16 | context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Helpers/JwtMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Helpers; 2 | 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.IdentityModel.Tokens; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Text; 7 | using WebApi.Services; 8 | 9 | public class JwtMiddleware 10 | { 11 | private readonly RequestDelegate _next; 12 | private readonly AppSettings _appSettings; 13 | 14 | public JwtMiddleware(RequestDelegate next, IOptions appSettings) 15 | { 16 | _next = next; 17 | _appSettings = appSettings.Value; 18 | } 19 | 20 | public async Task Invoke(HttpContext context, IUserService userService) 21 | { 22 | var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); 23 | 24 | if (token != null) 25 | attachUserToContext(context, userService, token); 26 | 27 | await _next(context); 28 | } 29 | 30 | private void attachUserToContext(HttpContext context, IUserService userService, string token) 31 | { 32 | try 33 | { 34 | var tokenHandler = new JwtSecurityTokenHandler(); 35 | var key = Encoding.ASCII.GetBytes(_appSettings.Secret); 36 | tokenHandler.ValidateToken(token, new TokenValidationParameters 37 | { 38 | ValidateIssuerSigningKey = true, 39 | IssuerSigningKey = new SymmetricSecurityKey(key), 40 | ValidateIssuer = false, 41 | ValidateAudience = false, 42 | // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) 43 | ClockSkew = TimeSpan.Zero 44 | }, out SecurityToken validatedToken); 45 | 46 | var jwtToken = (JwtSecurityToken)validatedToken; 47 | var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value); 48 | 49 | // attach user to context on successful jwt validation 50 | context.Items["User"] = userService.GetById(userId); 51 | } 52 | catch 53 | { 54 | // do nothing if jwt validation fails 55 | // user is not attached to context so request won't have access to secure routes 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Jason Watmore 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Models/AuthenticateRequest.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Models; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | public class AuthenticateRequest 6 | { 7 | [Required] 8 | public string Username { get; set; } 9 | 10 | [Required] 11 | public string Password { get; set; } 12 | } -------------------------------------------------------------------------------- /Models/AuthenticateResponse.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Models; 2 | 3 | using WebApi.Entities; 4 | 5 | public class AuthenticateResponse 6 | { 7 | public int Id { get; set; } 8 | public string FirstName { get; set; } 9 | public string LastName { get; set; } 10 | public string Username { get; set; } 11 | public string Token { get; set; } 12 | 13 | 14 | public AuthenticateResponse(User user, string token) 15 | { 16 | Id = user.Id; 17 | FirstName = user.FirstName; 18 | LastName = user.LastName; 19 | Username = user.Username; 20 | Token = token; 21 | } 22 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using WebApi.Helpers; 2 | using WebApi.Services; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | // add services to DI container 7 | { 8 | var services = builder.Services; 9 | services.AddCors(); 10 | services.AddControllers(); 11 | 12 | // configure strongly typed settings object 13 | services.Configure(builder.Configuration.GetSection("AppSettings")); 14 | 15 | // configure DI for application services 16 | services.AddScoped(); 17 | } 18 | 19 | var app = builder.Build(); 20 | 21 | // configure HTTP request pipeline 22 | { 23 | // global cors policy 24 | app.UseCors(x => x 25 | .AllowAnyOrigin() 26 | .AllowAnyMethod() 27 | .AllowAnyHeader()); 28 | 29 | // custom jwt auth middleware 30 | app.UseMiddleware(); 31 | 32 | app.MapControllers(); 33 | } 34 | 35 | app.Run("http://localhost:4000"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-6-jwt-authentication-api 2 | 3 | .NET 6.0 - JWT Authentication API 4 | 5 | Documentation at https://jasonwatmore.com/post/2021/12/14/net-6-jwt-authentication-tutorial-with-example-api 6 | -------------------------------------------------------------------------------- /Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Services; 2 | 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.IdentityModel.Tokens; 5 | using System.IdentityModel.Tokens.Jwt; 6 | using System.Security.Claims; 7 | using System.Text; 8 | using WebApi.Entities; 9 | using WebApi.Helpers; 10 | using WebApi.Models; 11 | 12 | public interface IUserService 13 | { 14 | AuthenticateResponse Authenticate(AuthenticateRequest model); 15 | IEnumerable GetAll(); 16 | User GetById(int id); 17 | } 18 | 19 | public class UserService : IUserService 20 | { 21 | // users hardcoded for simplicity, store in a db with hashed passwords in production applications 22 | private List _users = new List 23 | { 24 | new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" } 25 | }; 26 | 27 | private readonly AppSettings _appSettings; 28 | 29 | public UserService(IOptions appSettings) 30 | { 31 | _appSettings = appSettings.Value; 32 | } 33 | 34 | public AuthenticateResponse Authenticate(AuthenticateRequest model) 35 | { 36 | var user = _users.SingleOrDefault(x => x.Username == model.Username && x.Password == model.Password); 37 | 38 | // return null if user not found 39 | if (user == null) return null; 40 | 41 | // authentication successful so generate jwt token 42 | var token = generateJwtToken(user); 43 | 44 | return new AuthenticateResponse(user, token); 45 | } 46 | 47 | public IEnumerable GetAll() 48 | { 49 | return _users; 50 | } 51 | 52 | public User GetById(int id) 53 | { 54 | return _users.FirstOrDefault(x => x.Id == id); 55 | } 56 | 57 | // helper methods 58 | 59 | private string generateJwtToken(User user) 60 | { 61 | // generate token that is valid for 7 days 62 | var tokenHandler = new JwtSecurityTokenHandler(); 63 | var key = Encoding.ASCII.GetBytes(_appSettings.Secret); 64 | var tokenDescriptor = new SecurityTokenDescriptor 65 | { 66 | Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), 67 | Expires = DateTime.UtcNow.AddDays(7), 68 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) 69 | }; 70 | var token = tokenHandler.CreateToken(tokenDescriptor); 71 | return tokenHandler.WriteToken(token); 72 | } 73 | } -------------------------------------------------------------------------------- /WebApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /omnisharp.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild": { 3 | "useBundledOnly": true 4 | } 5 | } --------------------------------------------------------------------------------