├── .gitignore ├── Authorization ├── AllowAnonymousAttribute.cs ├── AuthorizeAttribute.cs ├── JwtMiddleware.cs └── JwtUtils.cs ├── Controllers └── UsersController.cs ├── Entities └── User.cs ├── Helpers └── AppSettings.cs ├── LICENSE ├── Models ├── AuthenticateRequest.cs └── AuthenticateResponse.cs ├── Program.cs ├── README.md ├── Services └── UserService.cs ├── WebApi.csproj └── appsettings.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 -------------------------------------------------------------------------------- /Authorization/AllowAnonymousAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Authorization; 2 | 3 | [AttributeUsage(AttributeTargets.Method)] 4 | public class AllowAnonymousAttribute : Attribute 5 | { } -------------------------------------------------------------------------------- /Authorization/AuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Authorization; 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 | // skip authorization if action is decorated with [AllowAnonymous] attribute 13 | var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType().Any(); 14 | if (allowAnonymous) 15 | return; 16 | 17 | // authorization 18 | var user = (User?)context.HttpContext.Items["User"]; 19 | if (user == null) 20 | { 21 | // not logged in or role not authorized 22 | context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /Authorization/JwtMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Authorization; 2 | 3 | using WebApi.Services; 4 | 5 | public class JwtMiddleware 6 | { 7 | private readonly RequestDelegate _next; 8 | 9 | public JwtMiddleware(RequestDelegate next) 10 | { 11 | _next = next; 12 | } 13 | 14 | public async Task Invoke(HttpContext context, IUserService userService, IJwtUtils jwtUtils) 15 | { 16 | var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); 17 | var userId = jwtUtils.ValidateJwtToken(token); 18 | if (userId != null) 19 | { 20 | // attach user to context on successful jwt validation 21 | context.Items["User"] = userService.GetById(userId.Value); 22 | } 23 | 24 | await _next(context); 25 | } 26 | } -------------------------------------------------------------------------------- /Authorization/JwtUtils.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Authorization; 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 | 11 | public interface IJwtUtils 12 | { 13 | public string GenerateJwtToken(User user); 14 | public int? ValidateJwtToken(string? token); 15 | } 16 | 17 | public class JwtUtils : IJwtUtils 18 | { 19 | private readonly AppSettings _appSettings; 20 | 21 | public JwtUtils(IOptions appSettings) 22 | { 23 | _appSettings = appSettings.Value; 24 | 25 | if (string.IsNullOrEmpty(_appSettings.Secret)) 26 | throw new Exception("JWT secret not configured"); 27 | } 28 | 29 | public string GenerateJwtToken(User user) 30 | { 31 | // generate token that is valid for 7 days 32 | var tokenHandler = new JwtSecurityTokenHandler(); 33 | var key = Encoding.ASCII.GetBytes(_appSettings.Secret!); 34 | var tokenDescriptor = new SecurityTokenDescriptor 35 | { 36 | Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), 37 | Expires = DateTime.UtcNow.AddDays(7), 38 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) 39 | }; 40 | var token = tokenHandler.CreateToken(tokenDescriptor); 41 | return tokenHandler.WriteToken(token); 42 | } 43 | 44 | public int? ValidateJwtToken(string? token) 45 | { 46 | if (token == null) 47 | return null; 48 | 49 | var tokenHandler = new JwtSecurityTokenHandler(); 50 | var key = Encoding.ASCII.GetBytes(_appSettings.Secret!); 51 | try 52 | { 53 | tokenHandler.ValidateToken(token, new TokenValidationParameters 54 | { 55 | ValidateIssuerSigningKey = true, 56 | IssuerSigningKey = new SymmetricSecurityKey(key), 57 | ValidateIssuer = false, 58 | ValidateAudience = false, 59 | // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) 60 | ClockSkew = TimeSpan.Zero 61 | }, out SecurityToken validatedToken); 62 | 63 | var jwtToken = (JwtSecurityToken)validatedToken; 64 | var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value); 65 | 66 | // return user id from JWT token if validation successful 67 | return userId; 68 | } 69 | catch 70 | { 71 | // return null if validation fails 72 | return null; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Controllers; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using WebApi.Authorization; 5 | using WebApi.Models; 6 | using WebApi.Services; 7 | 8 | [ApiController] 9 | [Authorize] 10 | [Route("[controller]")] 11 | public class UsersController : ControllerBase 12 | { 13 | private IUserService _userService; 14 | 15 | public UsersController(IUserService userService) 16 | { 17 | _userService = userService; 18 | } 19 | 20 | [AllowAnonymous] 21 | [HttpPost("authenticate")] 22 | public IActionResult Authenticate(AuthenticateRequest model) 23 | { 24 | var response = _userService.Authenticate(model); 25 | 26 | if (response == null) 27 | return BadRequest(new { message = "Username or password is incorrect" }); 28 | 29 | return Ok(response); 30 | } 31 | 32 | [HttpGet] 33 | public IActionResult GetAll() 34 | { 35 | var users = _userService.GetAll(); 36 | return Ok(users); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 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.Authorization; 2 | using WebApi.Helpers; 3 | using WebApi.Services; 4 | 5 | var builder = WebApplication.CreateBuilder(args); 6 | 7 | // add services to DI container 8 | { 9 | var services = builder.Services; 10 | services.AddCors(); 11 | services.AddControllers(); 12 | 13 | // configure strongly typed settings object 14 | services.Configure(builder.Configuration.GetSection("AppSettings")); 15 | 16 | // configure DI for application services 17 | services.AddScoped(); 18 | services.AddScoped(); 19 | } 20 | 21 | var app = builder.Build(); 22 | 23 | // configure HTTP request pipeline 24 | { 25 | // global cors policy 26 | app.UseCors(x => x 27 | .AllowAnyOrigin() 28 | .AllowAnyMethod() 29 | .AllowAnyHeader()); 30 | 31 | // custom jwt auth middleware 32 | app.UseMiddleware(); 33 | 34 | app.MapControllers(); 35 | } 36 | 37 | app.Run("http://localhost:4000"); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-7-jwt-authentication-api 2 | 3 | .NET 7.0 - JWT Authentication API 4 | 5 | Documentation at https://jasonwatmore.com/net-7-csharp-jwt-authentication-tutorial-without-aspnet-core-identity 6 | -------------------------------------------------------------------------------- /Services/UserService.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Services; 2 | 3 | using WebApi.Authorization; 4 | using WebApi.Entities; 5 | using WebApi.Models; 6 | 7 | public interface IUserService 8 | { 9 | AuthenticateResponse? Authenticate(AuthenticateRequest model); 10 | IEnumerable GetAll(); 11 | User? GetById(int id); 12 | } 13 | 14 | public class UserService : IUserService 15 | { 16 | // users hardcoded for simplicity, store in a db with hashed passwords in production applications 17 | private List _users = new List 18 | { 19 | new User { Id = 1, FirstName = "Test", LastName = "User", Username = "test", Password = "test" } 20 | }; 21 | 22 | private readonly IJwtUtils _jwtUtils; 23 | 24 | public UserService(IJwtUtils jwtUtils) 25 | { 26 | _jwtUtils = jwtUtils; 27 | } 28 | 29 | public AuthenticateResponse? Authenticate(AuthenticateRequest model) 30 | { 31 | var user = _users.SingleOrDefault(x => x.Username == model.Username && x.Password == model.Password); 32 | 33 | // return null if user not found 34 | if (user == null) return null; 35 | 36 | // authentication successful so generate jwt token 37 | var token = _jwtUtils.GenerateJwtToken(user); 38 | 39 | return new AuthenticateResponse(user, token); 40 | } 41 | 42 | public IEnumerable GetAll() 43 | { 44 | return _users; 45 | } 46 | 47 | public User? GetById(int id) 48 | { 49 | return _users.FirstOrDefault(x => x.Id == id); 50 | } 51 | } -------------------------------------------------------------------------------- /WebApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net7.0 4 | enable 5 | enable 6 | WebApi 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------