JWT stands for , and it is an authorization mechanism, not authentication. So let’s figure out what the difference between those two. JSON Web Token is the mechanism that allows verifying that the user is exactly the one he claims to be. It is a login process where a user provides a username and password, and the system verifies them. So authentication answers the question: who is the user? Authentication is the mechanism that allows verification of which access rights the user has to a certain resource. It is a process of granting users some roles and a set of permissions a particular role has. So, authorization answers that question: what rights does the user have in the system? Authorization It is important to understand that Authentication always comes first and Authorization is second. In other words, you can’t get permission before you verify your identity. But what are the most popular authorization methods? There are two main approaches to handling authorization for the web application. Sessions A traditional approach on the web for authorization users is a cookie-based server-side session. The process starts when a user logs in and a server authenticates him. After that, the server creates a session with a Session ID and stores it somewhere in the server’s memory. The server sends back Session ID to the client and the client stores the Session ID in cookies. For every request, the client sends a Session ID as a part of the request, and the server verifies the Session ID in its memory and the user’s permissions related to this session. Tokens Another popular approach is using tokens for authorization. The process starts similarly when a user enters login, and passwords and a client sends a login request to a server. Instead of creating a session, the server generates a token signed with the secret token. Then, the server sends the token back to the client, and the client has to store it in a local storage. Similar to the session-based approach, the client has to send a token to the server for every request. However, the server does not store any additional information about the user session. The server has to validate that the token has not changed since it was created and signed with the secret key. Session vs Token Session-based authorization approach can be vulnerable to an attack known as Cross-Site Request Forgery (CSRF). It is a kind of attack when the attacker points to a site they are logged into to perform actions they didn’t intend to, like submitting a payment or changing a password. Another thing is that when using a session-based authorization approach creates a stateful session between a client and server. The problem is if a client wants to access different servers in the scope of the same application, those servers have to share a session state. In another case, the client will need to be authorized on each server since the session is going to be different. On the other hand, the token-based authorization approach does not require storing session data on the server side and may simplify authorization between multiple servers. However, tokens can still be stolen by an attacker and it also can be difficult to invalidate tokens. We will see the details and how to handle invalidation further in this article. JWT JSON Web Token (JWT) is an open standard that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the algorithm) or a public/private key pair using or . HMAC RSA ECDSA JWT structure JSON Web Tokens consist of three parts separated by dots . Header { "alg": "HS256", "typ": "JWT" } The header usually consists of two parts: the type of token, and the signing algorithm being used. Payload { "sub": "1234567890", "name": "John Doe", "admin": true } The payload contains the claims, which are statements about the user. The payload is then encoded to form the second part of the JSON Web Token. You can find a description of standard fields that are used as claims . Base64Url here Signature HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) To create the signature part, you have to take the encoded header, the encoded payload, a secret, and the algorithm specified in the header and sign that. The token typically looks like the following: xxxxx.yyyyy.zzzzz You can navigate to and debug a sample token or your own. Just paste your token into the field and select the of the token signature. jwt.io Encoded Algorithm .NET project Now that we have theoretical knowledge of how JWT works, we can apply it to the real-life project. Let’s assume we have a simple API that represents CRUD operations for the coffee entity. We are going to create an ASP.NET Core API project that represents Coffee API. After that, we will create another ASP.NET Core API project that would represent an Identity API that could generate JWT. In real life, you would probably use or , or for Authentication/Authorization purposes. However, we would create our own Identity API to demonstrate how to generate JWT. When Identity API is done, we can call its controller and generate JWT based on the user’s data. Also, we can protect the Coffee API with an authorization configuration that requires passing JWT with each request. Identity Server Okta Auth0 Coffee API First, we are going to create a simple ASP.NET Core API project that represents Coffee API. Here is the structure of this project: Let’s start with the in the folder. It is a simple entity with an and a properties. Coffee.cs Model Id Name namespace Hackernoon.Coffee.API.Model; public class Coffee { public int Id { get; set; } public string Name { get; set; } } We need to store our entities while working with the API. So, let’s introduce a simple in-memory storage. It is located in the file in the folder. Storage.cs Data namespace Hackernoon.Coffee.API.Data; public static class Storage { private static readonly List<Model.Coffee> Data = new(); public static List<Model.Coffee> GetAll() { return Data; } public static bool Create(Model.Coffee model) { if (Data.Any(c => c.Id == model.Id || c.Name == model.Name)) return false; Data.Add(new Model.Coffee { Id = model.Id, Name = model.Name }); return true; } public static bool Delete(int id) { if (Data.All(c => c.Id != id)) return false; Data.Remove(Storage.Data.First(c => c.Id == id)); return true; } public static bool Update(Model.Coffee model) { if (Data.All(c => c.Id != model.Id)) return false; Data.First(c => c.Id == model.Id).Name = model.Name; return true; } } We need a class that would represent requests to the Coffee API. So, let’s create in the folder. CoffeeRequest.cs Contracts namespace Hackernoon.Coffee.API.Contracts; public class CoffeeRequest { public int Id { get; set; } public string Name { get; set; } } When it is done, we can implement in the folder that represents CRUD operations for the coffee entity. CoffeeController.cs Controller using Hackernoon.Coffee.API.Contracts; using Hackernoon.Coffee.API.Data; using Microsoft.AspNetCore.Mvc; namespace Hackernoon.Coffee.API.Controllers; [Route("coffee")] [ApiController] public class CoffeeController : ControllerBase { [HttpGet] public IList<Model.Coffee> GetAll() { return Storage.GetAll(); } [HttpPost] public IActionResult Create([FromBody]CoffeeRequest request) { var model = new Model.Coffee { Id = request.Id, Name = request.Name }; if (!Storage.Create(model)) return new BadRequestResult(); return new OkResult(); } [HttpDelete] public IActionResult Delete(int id) { if (!Storage.Delete(id)) return new BadRequestResult(); return new OkResult(); } [HttpPut] public IActionResult Update([FromBody] CoffeeRequest request) { var model = new Model.Coffee() { Id = request.Id, Name = request.Name }; if (!Storage.Update(model)) return new BadRequestResult(); return new OkResult(); } } Coffee API is done, and we can run the project and see Swagger UI as follows: Identity API Let’s create another ASP.NET Core API project that represents Identity API. Here is the structure of this project: Let’s start with the in folder, which represents the request for the generation of a new JWT with and properties. TokenGenerationRequest.cs Contracts Email Password namespace Hackernoon.Identity.API.Contracts; public class TokenGenerationRequest { public string Email { get; set; } public string Password { get; set; } } We need to implement only that represents the logic of generation JWT. But before we do that NuGet package needs to be installed. TokenController.cs Microsoft.AspNetCore.Authentication.JwtBearer using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using Hackernoon.Identity.API.Contracts; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Tokens; namespace Hackernoon.Identity.API.Controllers; [Route("token")] public class TokenController : ControllerBase { private const string SecretKey = "VerySecretAndLongKey-NeedMoreSymbolsHere-123"; private const string Issuer = "IdentityServerIssuer"; private const string Audience = "IdentityServerClient"; private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(20); [HttpPost] public string Create([FromBody]TokenGenerationRequest request) { var claims = new List<Claim> {new Claim(ClaimTypes.Email, request.Email) }; var jwt = new JwtSecurityToken( issuer: Issuer, audience: Audience, claims: claims, expires: DateTime.UtcNow.Add(Lifetime), signingCredentials: CreateSigningCredentials()); return new JwtSecurityTokenHandler().WriteToken(jwt); } private static SigningCredentials CreateSigningCredentials() { return new SigningCredentials( new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)), SecurityAlgorithms.HmacSha256); } } Note that sensitive const such as , , and have to be put somewhere in the configuration. They are hardcoded just for simplifying this test project. The field is set to 20 minutes, which means that the token will be valid for that time. You also might configure this parameter. SecretKey Issuer Audience Lifetime Now we can run the project and see Swagger UI as follows: Let’s make a call to the endpoint and generate a new JWT. Try the following payload: /token { "email": "john.doe@gmail.com", "password": "password" } Identity API will generate the corresponding JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9lbWFpbGFkZHJlc3MiOiJqb2huLmRvZUBnbWFpbC5jb20iLCJJc0dvdXJtZXQiOiJmYWxzZSIsImV4cCI6MTcwNzc4Mzk4MCwiaXNzIjoiSWRlbnRpdHlTZXJ2ZXJJc3N1ZXIiLCJhdWQiOiJJZGVudGl0eVNlcnZlckNsaWVudCJ9.4odXsbWak1C0uK3Ux-n7f58icYQQwlHjM54OjgMCVPM Enabling Authorization in Coffee API Now, when Identity API is ready and provides us with tokens, we can guard Coffee API with authorization. Again NuGet package needs to be installed. Microsoft.AspNetCore.Authentication.JwtBearer We need to register the required services by authentication services. Add the following code to the file right after creating a builder. Program.cs var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = "IdentityServerIssuer", ValidateAudience = true, ValidAudience = "IdentityServerClient", ValidateLifetime = true, IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes("VerySecretAndLongKey-NeedMoreSymbolsHere-123")), ValidateIssuerSigningKey = true, }; }); builder.Services.AddAuthorization(); It is important to remember that order in middleware is important. We enable authentication by calling method and specifying as an authentication schema. It is a constant that contains a value. AddAuthentication() JwtBearerDefaults.AuthenticationScheme Bearer namespace Microsoft.AspNetCore.Authentication.JwtBearer { /// <summary>Default values used by bearer authentication.</summary> public static class JwtBearerDefaults { /// <summary> /// Default value for AuthenticationScheme property in the JwtBearerAuthenticationOptions /// </summary> public const string AuthenticationScheme = "Bearer"; } } We need to specify that describes which parameters of JWT will be validated during the authorization. We also specify similar to the in Identity API to verify the JWT signature. Check more details about . TokenValidationParameters IssuerSigningKey signingCredentials TokenValidationParameters here The next piece of code adds middleware to the builder that enables authentication and authorization capabilities. It should be added between the and methods. UseHttpsRedirection() MapControllers() app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); Now, we can use the attribute over the controller or its actions. By applying this code, now all the actions in are protected with an authorization mechanism, and JWT has to be sent as a part of the request. Authorize CoffeeController [Route("coffee")] [ApiController] [Authorize] public class CoffeeController : ControllerBase { .. If we make a call to any endpoint of the Coffee API, we can debug and see that it is populated and has an with claims we have specified in JWT. It is an important thing in understanding how ASP.NET Core handles Authorization under the hood. HttpContext.User Identity Add Authorization to Swagger UI We did great work to protect Coffee API with the authorization. But if you run the Coffee API project and open Swagger UI, you won’t be able to send JWT as a part of the request. To fix that, we need to update the file with the following code: Program.cs builder.Services.AddSwaggerGen(option => { option.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { In = ParameterLocation.Header, Description = "Please enter a valid token", Name = "Authorization", Type = SecuritySchemeType.Http, BearerFormat = "JWT", Scheme = "Bearer" }); option.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type=ReferenceType.SecurityScheme, Id="Bearer" } }, new string[]{} } }); }); After that, we will be able to see the button at the right top corner: Authorize When you click on the button you will be able to enter JWT as follows: Authorize Use Postman for testing You can not limit yourself to using Swagger UI and can perform testing of the API through the Postman tool. Let’s call endpoint of the Identity API first. We need to specify header with the value in the section since we are going to use JSON as a payload. /token Content-Type application/json Headers After that, we can call endpoint and get a new JWT. /token Now, we can copy JWT and use it to call Coffee API. We need to specify header similar to the Identity API if we want to test, create, and update endpoints. header also has to be set with the value . After that, just hit button and see the result. Content-Type Authorization Bearer [your JWT value] Send Role-based authorization As you remember, the payload part of JWT is a set of claims with values that are exactly key-value pairs. Role-based authorization allows you to differentiate access to application resources depending on the role to which the user belongs. If we update the method in the file in Identity API with the code that adds a new claim for the role; we can handle role-based authentication in the Coffee API. is a predefined name of the role claim. Create() TokenController.cs ClaimTypes.Role var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim(ClaimTypes.Role, "Barista") }; Update the attribute in the file specifying the role name: Authorize CoffeeController.cs [Authorize(Roles = "Barista")] Now, all users who make a call to Coffee API have to have the role claim with the value. Otherwise, they will get status code. Barista 403 Forbidden Claim-based authorization An attribute can easily handle role-based authentication. But what if it is not enough, and we want to differentiate access based on some user properties like age or any other? You have probably already guessed that you can add your claims to JWT and use them to build authorization logic. Role-based authorization itself is a special case of claims-based authorization, just as a role is the same claim object of a predefined type. Authorize Let’s update the method in the file in Identity API with the code that adds a new claim . Create() TokenController.cs IsGourmet var claims = new List<Claim> { new Claim(ClaimTypes.Email, request.Email), new Claim("IsGourmet", "true") }; In the Program.cs file in Coffee API, we need to create a policy that verifies a claim and can be used in the attribute. The following code has to be added right after the method call. Authorize AddAuthentication() builder.Services.AddAuthorization(opts => { opts.AddPolicy("OnlyForGourmet", policy => { policy.RequireClaim("IsGourmet", "true"); }); }); Update the attribute in the file specifying the policy name: Authorize CoffeeController.cs [Authorize(Policy = "OnlyForGourmet")] Summary Congratulations! You made a great effort in learning JWT in .NET. Now, you have to have a solid understanding of JWT principles and why it is important to use it to perform authorization in .NET applications. But we just scratched the surface in the area of authentication and authorization in ASP.NET Core applications. I suggest looking into Microsoft documentation regarding the topics we discussed in this article. There are also a lot of built-in capabilities for authorization and role management in the .NET platform. A good addition to this article could be Microsoft about authorization. documentation