├── .gitignore ├── JWT.sln ├── JWT1 ├── Controllers │ ├── BooksController.cs │ └── TokenController.cs ├── JWT.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── README.md └── Test ├── ApplicationTest.cs ├── Test.csproj └── appsettings.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.vs 2 | /JWT1/*.user 3 | /JWT1/bin 4 | /JWT1/obj 5 | /Test/bin 6 | /Test/obj 7 | -------------------------------------------------------------------------------- /JWT.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2009 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JWT", "JWT1\JWT.csproj", "{8F9C1287-62BE-46E7-9452-4719DBFC9D63}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{2C32F0C7-55C8-4B6A-BCE4-A00D5C9D48E5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8F9C1287-62BE-46E7-9452-4719DBFC9D63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {8F9C1287-62BE-46E7-9452-4719DBFC9D63}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {8F9C1287-62BE-46E7-9452-4719DBFC9D63}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {8F9C1287-62BE-46E7-9452-4719DBFC9D63}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2C32F0C7-55C8-4B6A-BCE4-A00D5C9D48E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2C32F0C7-55C8-4B6A-BCE4-A00D5C9D48E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2C32F0C7-55C8-4B6A-BCE4-A00D5C9D48E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2C32F0C7-55C8-4B6A-BCE4-A00D5C9D48E5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {DF210460-8A63-4839-A232-E132CAD04D36} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /JWT1/Controllers/BooksController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Security.Claims; 7 | 8 | namespace JWT.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | public class BooksController : Controller 12 | { 13 | [HttpGet, Authorize] 14 | public IEnumerable Get() 15 | { 16 | var currentUser = HttpContext.User; 17 | int userAge = 0; 18 | var resultBookList = new Book[] { 19 | new Book { Author = "Ray Bradbury", Title = "Fahrenheit 451", AgeRestriction = false }, 20 | new Book { Author = "Gabriel García Márquez", Title = "One Hundred years of Solitude", AgeRestriction = false }, 21 | new Book { Author = "George Orwell", Title = "1984", AgeRestriction = false }, 22 | new Book { Author = "Anais Nin", Title = "Delta of Venus", AgeRestriction = true } 23 | }; 24 | 25 | if (currentUser.HasClaim(c => c.Type == ClaimTypes.DateOfBirth)) 26 | { 27 | DateTime birthDate = DateTime.Parse(currentUser.Claims.FirstOrDefault(c => c.Type == ClaimTypes.DateOfBirth).Value); 28 | userAge = DateTime.Today.Year - birthDate.Year; 29 | } 30 | 31 | if (userAge < 18) 32 | { 33 | resultBookList = resultBookList.Where(b => !b.AgeRestriction).ToArray(); 34 | } 35 | 36 | return resultBookList; 37 | } 38 | 39 | public class Book 40 | { 41 | public string Author { get; set; } 42 | public string Title { get; set; } 43 | public bool AgeRestriction { get; set; } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /JWT1/Controllers/TokenController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Extensions.Configuration; 4 | using Microsoft.IdentityModel.Tokens; 5 | using System; 6 | using System.IdentityModel.Tokens.Jwt; 7 | using System.Security.Claims; 8 | using System.Text; 9 | 10 | namespace JWT.Controllers 11 | { 12 | [Route("api/[controller]")] 13 | public class TokenController : Controller 14 | { 15 | private IConfiguration _config; 16 | 17 | public TokenController(IConfiguration config) 18 | { 19 | _config = config; 20 | } 21 | 22 | [AllowAnonymous] 23 | [HttpPost] 24 | public IActionResult CreateToken([FromBody]LoginModel login) 25 | { 26 | IActionResult response = Unauthorized(); 27 | var user = Authenticate(login); 28 | 29 | if (user != null) 30 | { 31 | var tokenString = BuildToken(user); 32 | response = Ok(new { token = tokenString }); 33 | } 34 | 35 | return response; 36 | } 37 | 38 | private string BuildToken(UserModel user) 39 | { 40 | 41 | var claims = new[] { 42 | new Claim(JwtRegisteredClaimNames.Sub, user.Name), 43 | new Claim(JwtRegisteredClaimNames.Email, user.Email), 44 | new Claim(JwtRegisteredClaimNames.Birthdate, user.Birthdate.ToString("yyyy-MM-dd")), 45 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) 46 | }; 47 | 48 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Jwt:Key"])); 49 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 50 | 51 | var token = new JwtSecurityToken(_config["Jwt:Issuer"], 52 | _config["Jwt:Issuer"], 53 | claims, 54 | expires: DateTime.Now.AddMinutes(30), 55 | signingCredentials: creds); 56 | 57 | return new JwtSecurityTokenHandler().WriteToken(token); 58 | } 59 | 60 | private UserModel Authenticate(LoginModel login) 61 | { 62 | UserModel user = null; 63 | 64 | if (login.Username == "mario" && login.Password == "secret") 65 | { 66 | user = new UserModel { Name = "Mario Rossi", Email = "mario.rossi@domain.com", Birthdate = new DateTime(1983, 9, 23)}; 67 | } 68 | 69 | if (login.Username == "mary" && login.Password == "barbie") 70 | { 71 | user = new UserModel { Name = "Mary Smith", Email = "mary.smith@domain.com", Birthdate = new DateTime(2001, 5, 13) }; 72 | } 73 | 74 | return user; 75 | 76 | } 77 | 78 | public class LoginModel 79 | { 80 | public string Username { get; set; } 81 | public string Password { get; set; } 82 | } 83 | 84 | private class UserModel 85 | { 86 | public string Name { get; set; } 87 | public string Email { get; set; } 88 | public DateTime Birthdate { get; set; } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /JWT1/JWT.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | JWT 6 | JWT 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /JWT1/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace JWT 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup() 16 | .Build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /JWT1/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:63939/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "api/books", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "JWT1": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "launchUrl": "api/books", 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | }, 26 | "applicationUrl": "http://localhost:63939/" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /JWT1/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.IdentityModel.Tokens; 7 | using System.Text; 8 | 9 | namespace JWT 10 | { 11 | public class Startup 12 | { 13 | public Startup(IConfiguration configuration) 14 | { 15 | Configuration = configuration; 16 | } 17 | 18 | public IConfiguration Configuration { get; } 19 | 20 | // This method gets called by the runtime. Use this method to add services to the container. 21 | public void ConfigureServices(IServiceCollection services) 22 | { 23 | services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) 24 | .AddJwtBearer(options => 25 | { 26 | options.TokenValidationParameters = new TokenValidationParameters 27 | { 28 | ValidateIssuer = true, 29 | ValidateAudience = true, 30 | ValidateLifetime = true, 31 | ValidateIssuerSigningKey = true, 32 | ValidIssuer = Configuration["Jwt:Issuer"], 33 | ValidAudience = Configuration["Jwt:Issuer"], 34 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:Key"])) 35 | }; 36 | }); 37 | 38 | services.AddMvc(); 39 | } 40 | 41 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 42 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 43 | { 44 | if (env.IsDevelopment()) 45 | { 46 | app.UseDeveloperExceptionPage(); 47 | } 48 | 49 | app.UseAuthentication(); 50 | 51 | app.UseMvc(); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /JWT1/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /JWT1/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | }, 15 | "Jwt": { 16 | "Key": "veryVerySecretKey", 17 | "Issuer": "http://localhost:63939/" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netcore2-jwt 2 | This is a .NET Core 2 sample project showing how to use JWTs (_JSON Web Tokens_). It is the companion code for the article [Securing ASP.NET Core 2.0 Applications with JWTs](https://auth0.com/blog/securing-asp-dot-net-core-2-applications-with-jwts/). 3 | 4 | The *JWT* project defines a Web API application whose authentication is based on JWT. 5 | 6 | 7 | ## Running the project ## 8 | 9 | 10 | The solution contains a _Test_ project with four integration tests validating the application behaviour. 11 | You can run the tests from Visual Studio 2017 or by typing `dotnet test` in a command window. 12 | 13 | If you want to interactively test the application, you can use [Postman](https://www.getpostman.com/) or any other Http client. 14 | 15 | 1. Run the project from Visual Studio 2017 or by typing `dotnet run` in a command window 16 | 2. Launch _Postman_ and make a GET request as follows: 17 | 18 | ``` 19 | GET http://localhost:63939/api/books HTTP/1.1 20 | cache-control: no-cache 21 | Accept: */* 22 | Host: localhost:63939 23 | accept-encoding: gzip, deflate 24 | Connection: keep-alive 25 | ``` 26 | 27 | This should return a 401 HTTP status code (_Unauthorized_) 28 | 29 | 3. Make a POST request like the following: 30 | 31 | ``` 32 | POST http://localhost:63939/api/token HTTP/1.1 33 | cache-control: no-cache 34 | Content-Type: application/json 35 | Accept: */* 36 | Host: localhost:63939 37 | accept-encoding: gzip, deflate 38 | content-length: 39 39 | Connection: keep-alive 40 | 41 | {username: "mario", password: "secret"} 42 | ``` 43 | 44 | It returns a JSON object like the following: 45 | 46 | ``` 47 | { 48 | "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImp0aSI6IjVkNTRkMzIwLWQ3N2EtNDFhMy1iZTcwLTc2M2UyMGRmMjE3MyIsImV4cCI6MTUxMTE3NzQwMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MzkzOS8iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjYzOTM5LyJ9.g0yooTf3DJO43yL8bT4VE_VIdc5WHFhCVb3u9Jg7VTk" 49 | } 50 | ``` 51 | 52 | 4. The following GET request 53 | 54 | ``` 55 | GET http://localhost:63939/api/books HTTP/1.1 56 | cache-control: no-cache 57 | Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJNYXJpbyBSb3NzaSIsImVtYWlsIjoibWFyaW8ucm9zc2lAZG9tYWluLmNvbSIsImp0aSI6IjVkNTRkMzIwLWQ3N2EtNDFhMy1iZTcwLTc2M2UyMGRmMjE3MyIsImV4cCI6MTUxMTE3NzQwMywiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo2MzkzOS8iLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjYzOTM5LyJ9.g0yooTf3DJO43yL8bT4VE_VIdc5WHFhCVb3u9Jg7VTk 58 | Accept: */* 59 | Host: localhost:63939 60 | accept-encoding: gzip, deflate 61 | Connection: keep-alive 62 | ``` 63 | 64 | returns the following response: 65 | 66 | ``` 67 | [ 68 | { 69 | "author": "Ray Bradbury", 70 | "title": "Fahrenheit 451", 71 | "ageRestriction": false 72 | }, 73 | { 74 | "author": "Gabriel García Márquez", 75 | "title": "One Hundred years of Solitude", 76 | "ageRestriction": false 77 | }, 78 | { 79 | "author": "George Orwell", 80 | "title": "1984", 81 | "ageRestriction": false 82 | }, 83 | { 84 | "author": "Anais Nin", 85 | "title": "Delta of Venus", 86 | "ageRestriction": true 87 | } 88 | ] 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /Test/ApplicationTest.cs: -------------------------------------------------------------------------------- 1 | using JWT; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.TestHost; 4 | using Microsoft.Extensions.Configuration; 5 | using Newtonsoft.Json.Linq; 6 | using System.IO; 7 | using System.Net; 8 | using System.Net.Http; 9 | using System.Net.Http.Headers; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | using Xunit; 13 | 14 | namespace Test 15 | { 16 | public class ApplicationTest 17 | { 18 | private readonly TestServer _server; 19 | private readonly HttpClient _client; 20 | public ApplicationTest() 21 | { 22 | var configuration = new ConfigurationBuilder() 23 | .SetBasePath(Path.GetFullPath(@"../../..")) 24 | .AddJsonFile("appsettings.json", optional: false) 25 | //.AddUserSecrets() 26 | .Build(); 27 | 28 | _server = new TestServer(new WebHostBuilder() 29 | .UseStartup() 30 | .UseConfiguration(configuration)); 31 | _client = _server.CreateClient(); 32 | } 33 | 34 | [Fact] 35 | public async Task UnAuthorizedAccess() 36 | { 37 | var response = await _client.GetAsync("/api/books"); 38 | 39 | Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode); 40 | } 41 | 42 | [Fact] 43 | public async Task GetToken() 44 | { 45 | var bodyString = @"{username: ""mario"", password: ""secret""}"; 46 | var response = await _client.PostAsync("/api/token", new StringContent(bodyString, Encoding.UTF8, "application/json")); 47 | 48 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 49 | 50 | var responseString = await response.Content.ReadAsStringAsync(); 51 | var responseJson = JObject.Parse(responseString); 52 | Assert.NotNull((string)responseJson["token"]); 53 | } 54 | 55 | [Fact] 56 | public async Task GetBooksWithoutAgeRestrictions() 57 | { 58 | var bodyString = @"{username: ""mario"", password: ""secret""}"; 59 | var response = await _client.PostAsync("/api/token", new StringContent(bodyString, Encoding.UTF8, "application/json")); 60 | var responseString = await response.Content.ReadAsStringAsync(); 61 | var responseJson = JObject.Parse(responseString); 62 | var token = (string)responseJson["token"]; 63 | 64 | var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/books"); 65 | requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 66 | var booksResponse = await _client.SendAsync(requestMessage); 67 | 68 | Assert.Equal(HttpStatusCode.OK, booksResponse.StatusCode); 69 | 70 | var bookResponseString = await booksResponse.Content.ReadAsStringAsync(); 71 | var bookResponseJson = JArray.Parse(bookResponseString); 72 | Assert.Equal(true, bookResponseJson.Count == 4); 73 | } 74 | 75 | [Fact] 76 | public async Task GetBooksWithAgeRestrictions() 77 | { 78 | var bodyString = @"{username: ""mary"", password: ""barbie""}"; 79 | var response = await _client.PostAsync("/api/token", new StringContent(bodyString, Encoding.UTF8, "application/json")); 80 | var responseString = await response.Content.ReadAsStringAsync(); 81 | var responseJson = JObject.Parse(responseString); 82 | var token = (string)responseJson["token"]; 83 | 84 | var requestMessage = new HttpRequestMessage(HttpMethod.Get, "/api/books"); 85 | requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token); 86 | var booksResponse = await _client.SendAsync(requestMessage); 87 | 88 | Assert.Equal(HttpStatusCode.OK, booksResponse.StatusCode); 89 | 90 | var bookResponseString = await booksResponse.Content.ReadAsStringAsync(); 91 | var bookResponseJson = JArray.Parse(bookResponseString); 92 | Assert.Equal(true, bookResponseJson.Count == 3); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Test/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "Debug": { 5 | "LogLevel": { 6 | "Default": "Warning" 7 | } 8 | }, 9 | "Console": { 10 | "LogLevel": { 11 | "Default": "Warning" 12 | } 13 | } 14 | }, 15 | "Jwt": { 16 | "Key": "veryVerySecretKey", 17 | "Issuer": "http://localhost:63939/" 18 | } 19 | } 20 | --------------------------------------------------------------------------------