├── .gitignore ├── Controllers ├── TokenController.cs └── UsersController.cs ├── JwtExample.csproj ├── Managers └── TokenManager.cs ├── Models ├── Authentication.cs ├── DatabaseSettings.cs ├── Tokens.cs └── User.cs ├── Program.cs ├── Properties └── launchSettings.json ├── README.md ├── Services └── UserService.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.*~ 3 | project.lock.json 4 | .DS_Store 5 | *.pyc 6 | nupkg/ 7 | 8 | # Visual Studio Code 9 | .vscode 10 | 11 | # Rider 12 | .idea 13 | 14 | # User-specific files 15 | *.suo 16 | *.user 17 | *.userosscache 18 | *.sln.docstates 19 | 20 | # Build results 21 | [Dd]ebug/ 22 | [Dd]ebugPublic/ 23 | [Rr]elease/ 24 | [Rr]eleases/ 25 | x64/ 26 | x86/ 27 | build/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Oo]ut/ 32 | msbuild.log 33 | msbuild.err 34 | msbuild.wrn -------------------------------------------------------------------------------- /Controllers/TokenController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Security.Claims; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Logging; 6 | using JwtExample.Models; 7 | using JwtExample.Services; 8 | using Microsoft.AspNetCore.Authorization; 9 | 10 | namespace backend.Controllers 11 | { 12 | [ApiController] 13 | [Route("[controller]")] 14 | public class TokensController : ControllerBase 15 | { 16 | private readonly ILogger _logger; 17 | private readonly UserService _userService; 18 | 19 | public TokensController(ILogger logger, UserService userService) 20 | { 21 | _logger = logger; 22 | _userService = userService; 23 | } 24 | 25 | [HttpPost("accesstoken", Name = "login")] 26 | public IActionResult Login([FromBody]Authentication auth) 27 | { 28 | try { 29 | return Ok(_userService.Login(auth)); 30 | } 31 | catch(Exception e) { 32 | return BadRequest(e); 33 | } 34 | } 35 | 36 | [Authorize(AuthenticationSchemes = "refresh")] 37 | [HttpPut("accesstoken", Name = "refresh")] 38 | public IActionResult Refresh() 39 | { 40 | Claim refreshtoken = User.Claims.FirstOrDefault(x => x.Type == "refresh"); 41 | Claim username = User.Claims.FirstOrDefault(x => x.Type == "username"); 42 | 43 | try 44 | { 45 | return Ok(_userService.Refresh(username, refreshtoken)); 46 | } 47 | catch (Exception e) 48 | { 49 | return BadRequest(e.Message); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Controllers/UsersController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Logging; 5 | using JwtExample.Models; 6 | using JwtExample.Services; 7 | using Microsoft.AspNetCore.Authentication.JwtBearer; 8 | using Microsoft.AspNetCore.Authorization; 9 | 10 | namespace backend.Controllers 11 | { 12 | [ApiController] 13 | [Route("[controller]")] 14 | public class UsersController : ControllerBase 15 | { 16 | private readonly ILogger _logger; 17 | private readonly UserService _userService; 18 | 19 | public UsersController(ILogger logger, UserService userService) 20 | { 21 | _logger = logger; 22 | _userService = userService; 23 | } 24 | 25 | [HttpGet] 26 | [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] 27 | public IEnumerable Get() 28 | { 29 | return _userService.Get(); 30 | } 31 | 32 | [HttpGet("{userId}", Name = "GetOne")] 33 | public User GetOne([FromRoute]string userId) 34 | { 35 | return _userService.Get(userId); 36 | } 37 | 38 | [HttpPost] 39 | public IActionResult CreateOne([FromBody]User user) 40 | { 41 | user = _userService.Create(user); 42 | return CreatedAtRoute("GetOne", new { userId = user.Id }, user); 43 | } 44 | 45 | [HttpPut("{userId}")] 46 | public User UpdateOne([FromRoute]string userId, [FromBody]User user) 47 | { 48 | _userService.Update(userId, user); 49 | return user; 50 | } 51 | 52 | [HttpDelete("{userId}")] 53 | public IActionResult DeleteOne([FromRoute]string userId) 54 | { 55 | _userService.Remove(userId); 56 | return NoContent(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /JwtExample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Managers/TokenManager.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography; 2 | using System; 3 | using System.Collections.Generic; 4 | using JwtExample.Models; 5 | using JWT.Builder; 6 | using JWT.Algorithms; 7 | using System.Text; 8 | 9 | namespace JwtExample.Managers 10 | { 11 | public static class TokenManager 12 | { 13 | private static readonly string _secret = "Superlongsupersecret!"; 14 | 15 | public static string GenerateAccessToken(User user) 16 | { 17 | return new JwtBuilder() 18 | .WithAlgorithm(new HMACSHA256Algorithm()) 19 | .WithSecret(Encoding.ASCII.GetBytes(_secret)) 20 | .AddClaim("exp", DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds()) 21 | .AddClaim("username", user.Username) 22 | .Issuer("JwtExample") 23 | .Audience("access") 24 | .Encode(); 25 | } 26 | 27 | public static (string refreshToken, string jwt) GenerateRefreshToken(User user) 28 | { 29 | var randomNumber = new byte[32]; 30 | using (var rng = RandomNumberGenerator.Create()){ 31 | rng.GetBytes(randomNumber); 32 | Convert.ToBase64String(randomNumber); 33 | } 34 | 35 | var randomString = System.Text.Encoding.ASCII.GetString(randomNumber); 36 | 37 | string jwt = new JwtBuilder() 38 | .WithAlgorithm(new HMACSHA256Algorithm()) 39 | .WithSecret(_secret) 40 | .AddClaim("exp", DateTimeOffset.UtcNow.AddHours(4).ToUnixTimeSeconds()) 41 | .AddClaim("refresh", randomString) 42 | .AddClaim("username", user.Username) 43 | .Issuer("JwtExample") 44 | .Audience("refresh") 45 | .Encode(); 46 | 47 | return (randomString, jwt); 48 | } 49 | 50 | public static IDictionary VerifyToken(string token) 51 | { 52 | return new JwtBuilder() 53 | .WithSecret(_secret) 54 | .MustVerifySignature() 55 | .Decode>(token); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Models/Authentication.cs: -------------------------------------------------------------------------------- 1 | namespace JwtExample.Models 2 | { 3 | public class Authentication 4 | { 5 | public string Username { get; set; } 6 | public string Password { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Models/DatabaseSettings.cs: -------------------------------------------------------------------------------- 1 | namespace JwtExample.Models 2 | { 3 | public class DatabaseSettings : IDatabaseSettings 4 | { 5 | public string ConnectionString { get; set; } 6 | public string DatabaseName { get; set; } 7 | } 8 | 9 | public interface IDatabaseSettings 10 | { 11 | string ConnectionString { get; set; } 12 | string DatabaseName { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /Models/Tokens.cs: -------------------------------------------------------------------------------- 1 | namespace JwtExample.Models 2 | { 3 | public class Tokens 4 | { 5 | public string AccessToken { get; set; } 6 | public string RefreshToken { get; set; } 7 | } 8 | } -------------------------------------------------------------------------------- /Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.Json.Serialization; 3 | using MongoDB.Bson; 4 | using MongoDB.Bson.Serialization.Attributes; 5 | 6 | namespace JwtExample.Models 7 | { 8 | public class User 9 | { 10 | [BsonId] 11 | [BsonRepresentation(BsonType.ObjectId)] 12 | public string Id { get; set; } 13 | public string Username { get; set; } 14 | public string Password { get; set; } 15 | public string Firstname { get; set; } 16 | public string Lastname { get; set; } 17 | 18 | [JsonIgnore] 19 | public List RefreshTokens { get; set; } 20 | } 21 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace JwtExample 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:32511", 8 | "sslPort": 44383 9 | } 10 | }, 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "weatherforecast", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "JwtExample": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JWT Example code 2 | 3 | This is example code for [A secure implementation of JSON Web Tokens (JWT) in C#](https://medium.com/swlh/a-secure-implementation-of-json-web-tokens-jwt-in-c-710d06ea243). 4 | 5 | Warning: This is very basic and doesn't do things like hashing passwords! Only to test JSON Web Tokens. 6 | 7 | To test this, you need to have Dotnet Core 3 and MongoDB installed. Settings are in appsettings.json. 8 | 9 | This uses the following packages: 10 | 11 | - [MongoDB.Driver](https://www.nuget.org/packages/MongoDB.Driver/2.10.3) 12 | - [Microsoft.AspNetCore.Authentication.JwtBearer](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) 13 | - [JWT](https://www.nuget.org/packages/JWT) 14 | 15 | After restoring the packages and starting it, do the following to test the JWT: 16 | 17 | 1. POST https://localhost:5001/users with the following body: 18 | 19 | ```javascript 20 | { 21 | "username": "yourusername", 22 | "password": "yourpassword" 23 | } 24 | ``` 25 | 26 | 2. POST https://localhost:5001/tokens/accesstoken with the same body. You get the following response: 27 | 28 | ```javascript 29 | { 30 | "accessToken": "*jwt*", 31 | "refreshToken": "*jwt*" 32 | } 33 | ``` 34 | 35 | 3. To test the access token do a GET request to https://localhost:5001/users. In the Authorization header of the request should be "Bearer " and then the access token you got in step 2. It will give you a response with the user you created in step 1. 36 | 37 | 4. To test the refresh token do a PUT request to https://localhost:5001/tokens/accesstoken. In the Authorization header of the request should be "Bearer " and then the refresh token you got in step 2. It should respond with a new access token and a new refresh token. Try again to test if the refresh token is deleted, which it should. 38 | -------------------------------------------------------------------------------- /Services/UserService.cs: -------------------------------------------------------------------------------- 1 | using JwtExample.Models; 2 | using JwtExample.Managers; 3 | using MongoDB.Driver; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | 8 | namespace JwtExample.Services 9 | { 10 | public class UserService 11 | { 12 | private readonly IMongoCollection _users; 13 | 14 | public UserService(IDatabaseSettings settings) 15 | { 16 | var client = new MongoClient(settings.ConnectionString); 17 | var database = client.GetDatabase(settings.DatabaseName); 18 | 19 | _users = database.GetCollection("users"); 20 | } 21 | 22 | public List Get() => 23 | _users.Find(user => true).ToList(); 24 | 25 | public User Get(string id) => 26 | _users.Find(user => user.Id == id).FirstOrDefault(); 27 | 28 | public User Create(User user) 29 | { 30 | _users.InsertOne(user); 31 | return user; 32 | } 33 | 34 | public Tokens Login(Authentication authentication) { 35 | User user = _users.Find(u => u.Username == authentication.Username).FirstOrDefault(); 36 | 37 | bool validPassword = user.Password == authentication.Password; 38 | 39 | if (validPassword) { 40 | var refreshToken = TokenManager.GenerateRefreshToken(user); 41 | 42 | if (user.RefreshTokens == null) 43 | user.RefreshTokens = new List(); 44 | 45 | user.RefreshTokens.Add(refreshToken.refreshToken); 46 | 47 | _users.ReplaceOne(u => u.Id == user.Id, user); 48 | 49 | return new Tokens 50 | { 51 | AccessToken = TokenManager.GenerateAccessToken(user), 52 | RefreshToken = refreshToken.jwt 53 | }; 54 | } 55 | else { 56 | throw new System.Exception("Username or password incorrect"); 57 | } 58 | } 59 | 60 | public Tokens Refresh(Claim userClaim, Claim refreshClaim) { 61 | User user = _users.Find(x => x.Username == userClaim.Value).FirstOrDefault(); 62 | 63 | if (user == null) 64 | throw new System.Exception("User doesn't exist"); 65 | 66 | if (user.RefreshTokens == null) 67 | user.RefreshTokens = new List(); 68 | 69 | string token = user.RefreshTokens.FirstOrDefault(x => x == refreshClaim.Value); 70 | 71 | if (token != null) { 72 | var refreshToken = TokenManager.GenerateRefreshToken(user); 73 | 74 | user.RefreshTokens.Add(refreshToken.refreshToken); 75 | 76 | user.RefreshTokens.Remove(token); 77 | 78 | _users.ReplaceOne(u => u.Id == user.Id, user); 79 | 80 | return new Tokens 81 | { 82 | AccessToken = TokenManager.GenerateAccessToken(user), 83 | RefreshToken = refreshToken.jwt 84 | }; 85 | } 86 | else { 87 | throw new System.Exception("Refresh token incorrect"); 88 | } 89 | } 90 | 91 | public void Update(string id, User userIn) => 92 | _users.ReplaceOne(user => user.Id == id, userIn); 93 | 94 | public void Remove(User userIn) => 95 | _users.DeleteOne(user => user.Id == userIn.Id); 96 | 97 | public void Remove(string id) => 98 | _users.DeleteOne(user => user.Id == id); 99 | } 100 | } -------------------------------------------------------------------------------- /Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Hosting; 9 | using Microsoft.AspNetCore.Authentication.JwtBearer; 10 | using Microsoft.IdentityModel.Tokens; 11 | using Microsoft.Extensions.Options; 12 | using JwtExample.Models; 13 | using JwtExample.Services; 14 | 15 | namespace JwtExample 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.Configure(Configuration.GetSection(nameof(DatabaseSettings))); 30 | 31 | services.AddSingleton(sp => sp.GetRequiredService>().Value); 32 | 33 | services.AddSingleton(); 34 | 35 | services.AddControllers(); 36 | 37 | JwtBearerOptions options(JwtBearerOptions jwtBearerOptions, string audience) { 38 | jwtBearerOptions.RequireHttpsMetadata = false; 39 | jwtBearerOptions.SaveToken = true; 40 | jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters 41 | { 42 | ValidateIssuerSigningKey = true, 43 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("Superlongsupersecret!")), 44 | ValidIssuer = "JwtExample", 45 | ValidateAudience = true, 46 | ValidAudience = audience, 47 | ValidateLifetime = true, //validate the expiration and not before values in the token 48 | ClockSkew = TimeSpan.FromMinutes(1) //1 minute tolerance for the expiration date 49 | }; 50 | if (audience == "access") 51 | { 52 | jwtBearerOptions.Events = new JwtBearerEvents 53 | { 54 | OnAuthenticationFailed = context => 55 | { 56 | if (context.Exception.GetType() == typeof(SecurityTokenExpiredException)) 57 | { 58 | context.Response.Headers.Add("Token-Expired", "true"); 59 | } 60 | return Task.CompletedTask; 61 | } 62 | }; 63 | } 64 | return jwtBearerOptions; 65 | } 66 | 67 | services.AddAuthentication(x => 68 | { 69 | x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 70 | x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 71 | }) 72 | .AddJwtBearer(jwtBearerOptions => options(jwtBearerOptions, "access")) 73 | .AddJwtBearer("refresh", jwtBearerOptions => options(jwtBearerOptions, "refresh")); 74 | } 75 | 76 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 77 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 78 | { 79 | if (env.IsDevelopment()) 80 | { 81 | app.UseDeveloperExceptionPage(); 82 | } 83 | 84 | app.UseHttpsRedirection(); 85 | 86 | app.UseRouting(); 87 | 88 | app.UseAuthorization(); 89 | 90 | app.UseEndpoints(endpoints => 91 | { 92 | endpoints.MapControllers(); 93 | }); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*", 10 | "DatabaseSettings": { 11 | "ConnectionString": "mongodb://localhost:27017", 12 | "DatabaseName": "JwtExample" 13 | } 14 | } 15 | --------------------------------------------------------------------------------