Introduction and project overview
Hello everybody. I'm Nick and in this video I'm going to show you how you can add the JSON Web Token authentication and authorization support in your ASP.NET Core application. If you like that content and you want to see more, make sure you subscribe, ring the subscribe notification bell, and for more training check out nickchaps ters.com. Okay, so let me show what I have here and actually I want to point out that I'm not going to be explaining what the JSON Web Token is or a JWT because I already have a video on that which you can check in the description below.
Running the Movies API and basic endpoints
There are also tons of other videos going really in depth with the topic. It is a generic topic, it's not specific to ASP.NET Core, and I want to focus on the implementation itself and show you how you can use it in a very nice way. Let me show what I have here. I have this Movies API and that's what we're going to be dealing with directly in this video and I'm going to run it to show you what this API can do. So API is running. I'm going to go to Postman and we can list all the movies in the system. Now unfortunately we don't really have any, so what I'm going to do is I'm going to create a new movie. I'm going to create the famous Comedy Nick the Greek, both the way back in 2010. So that is now created in the system. I can list it in that previous endpoint as you can see or I can get it using an ID. Now, oh, I made a bit of a mistake. It's actually released in 2023 and it's also an action movie not just a comedy. So I can update it and in the end I can simply just delete it. Now this API does not have any form of authentication or authorization. I don't need to be someone or validate that I am someone to be able to call it.
Adding the JWT Bearer NuGet package and configuration basics
But my controller looks just like this. I have an ApiController attribute around implementing ControllerBase and that's how my endpoints look. Now don't worry that I'm using controllers. I'm also going to cover minimal APIs in a second later in the video. So how do I add JSON Web Token support? Well, it all starts with a NuGet package. I'm going to go to NuGet packages and I'm going to add the Microsoft.AspNetCore.Authentication.JwtBearer. And yes, I know it's pronounced Jwt, just for some reason it's a W, but whatever. Configuration thankfully is actually pretty simple. It could be way, way worse. So the first thing you need to do is add, actually say services.AddAuthentication. Authentication comes before authorization and just to quickly recap for the avoidance of any doubt, authentication is the process of verifying that someone is who they say they are while authorization is the process of verifying that someone has access to what they say they have access to, or just saying what they have access to. So a lot we have this NuGet package. I can use the JwtBearerDefaults and I can use the default authentication scheme here, but another approach if you want to just update all the defaults is to do something like this: you can set the DefaultAuthenticateScheme, the DefaultChallengeScheme and the DefaultScheme all at once. And now with that NuGet package we also get this AddJwtBearer method which adds the support we need.
JWT structure and what to validate
So in here we're going to configure what we want to validate for our JWT. And just to quickly recap what a JWT looks like, here I have a JWT generated from one of my own services. We're going to be taking a look at that in a second and first we have the header which contains the type of the token and also the algorithm used to generate the signature. Then we have a bunch of standard claims and custom ones. JWT ID is a standard one, same with subject and all of these ones over here, and then we have a few custom ones: email, userId, admin and trusted member which I created. And then you have an issuer and an audience. The issuer is who created the token and audience is who this token is intended for. So think of, I don't know, Google authentication creating a token for you to use in Google Slides. Maybe you don't have access to Google Docs with that thing, so that token can only be used in Google Slides even though it was generated by Google and it is a valid token. And then in the end both the header and the payload are actually Base64 URL encoded, separated with a dot, and then there has to generate the signature. Hashing is not the only mechanism by the way to do this; you can have public key cryptography as well and other approaches, but this is by far the most common. So a few things you want to validate is the issuer, the audience, and also that the token is not expired. On top of that we want to validate the signature that it is a valid one. We can validate all of that by configuring the TokenValidationParameters value. We can create a new type over here.
Token validation parameters and secure key storage
And what I'm going to do before I paste what goes here is actually add some settings. For the purposes of this video I'm going to keep this here; however, you would not store the key here. You would store it securely in a service specifically designed to store tokens like Azure Key Vault or AWS Secrets Manager. If you want to know how you can do this with AWS Secret Manager I actually have a video on that topic. But you should never have it in the settings like this. Not only is it not secure, but if you want to rotate that token as the application is running this limits you significantly. You have to redeploy and then you have to keep track of multiple keys. It becomes very tricky. But for this video I'm just going to leave it here just so you can grab the code from the description and use it for your own purposes. So now with these things set, I'm going to just show you all the parameters we have to specify here and these are sort of the minimum ones you need. So we have the ValidIssuer which will pass down from configuration, ValidAudience as well, then the IssuerSigningKey and that is a new SymmetricSecurityKey in this case which we get from the settings. Again, like I said, you really want to load this from somewhere that is designed to store and load these things. And then we say that we want to validate the issuer, the audience, the lifetime of the token so that it is in effect because you can actually future-issue one, but also that it is not expired. So all of these things have to be specified as true and also that you have to validate the IssuerSigningKey because that's ultimately what makes the JWT so powerful: the fact that it is this stateless construct that all you need to do is validate the signature and then the rest is valid and true. Because if the signature is valid then the rest of the thing must be, because for the hash to be valid the payload and the header have to also be valid.
Enabling Authorization middleware and testing 401
So this is everything we need and then the next thing we need is the AddAuthorization call. So I'm going to add it as it is, bare bones, nothing in it. And then I'm going to go all the way down to the middleware area and after the HTTPS redirection and before all the MapControllers I'm going to say app.UseAuthentication and then app.UseAuthorization. Now remember that sequence in middleware actually matters. So authentication happens first, authorization second, then event controllers, meaning that everything above it are not part of authentication or authorization, so be very careful. Now actually in terms of authentication that is it. Obviously if I just leave my controller as it is and I go and I run it, nothing really will happen. I will still be able to get all my movies in the database. Now I have nothing of course, but nothing really blocks me from getting them. But if I now go in the controller and I say [Authorize] the users trying to access this controller and I try to run it again, then I'm going to get a 401 Unauthorized because nothing can authenticate me or authorize me to see if I can actually use this API.
Generating a JWT for testing and passing it via Bearer header
So how do we pass a JSON Web Token to validate that we can actually access it? Well first we create it and I have a helper identity API here that knows how to create a token. It uses the same key, which again you don't want to store here as a constant. Like I said, just store it somewhere securely and we're going to create a token that lasts for eight hours. This is way too long usually for JSON Web Tokens; they're way more short-lived and then recreated. And if you want to see a video on refreshing tokens leave a comment down below as well. But then this is the process I can follow to generate a JSON Web Token with all of my custom claims over here. You can just grab this and use it for your own purposes. Now in reality you will be using something like IdentityServer or Okta or Auth0 or some service dedicated to creating these types of things in specific flows, but because what these services automatically generate is a JSON Web Token we're just going to focus on the implementation side of things. We're not going to focus on the creation side of things. So we're just going to use this helper service. I'm just going to run it and go back to Postman and I'm just going to say that hey this is me, this is my user Id, create a token. And now I have a token to use. So I'm going to stop this API and run the Movies API. Remember we cannot access this because we're not authenticated but to say that hey it's Nick can I please access this, all I'm going to do is go to the headers and specify the Authorization header. But it's not just pasting the token; you actually have to say Bearer space and then the token. It's just the way of passing down a bearer token. And now if I have that I can go ahead and I can get the item. So I am authenticated and in this case authorized as well because we don't really check for anything. If the signature was different, if in the end I have a zero not an O, then I'm going to get an Unauthorized because the signature is invalid. If this thing was expired then I would get Unauthorized as well. But in this case I have a good token, so nothing really breaks. Everything just works.
Selecting which endpoints require auth and AllowAnonymous
Now let's talk about the very important thing: what if I want my Create, Update and Delete to be authenticated then authorized, but my Get doesn't really matter, I want everyone to be able to access it? You have two approaches here. You can either say [AllowAnonymous] here and this AllowAnonymous attribute will override the overarching controller-based behavior, or we can just take the [Authorize] from the controller and we can paste it on each individual thing we want authorized. You can ultimately choose which one you want to use. In general having it on a per-action basis is actually more used mainly because it allows you to specify different things on the action itself. And this is actually where authorization comes in, or the process of validating that someone can do what they claim to do. For example I want everyone who's authenticated to create and update a movie but I want only admins or administrators to delete a movie. How do I do that? Well it's a few ways but the simplest one is by using a claim. So in that JSON Web Token, and if you remember what I said, a custom claim is that admin is false. Now usually when someone is not something you wouldn't have it in the token, you will just not have the claim at all. The absence of a claim means that that user is not something. But if I just run this API again and I regenerate this token and I say that yes this is actually an admin, then I have a token of someone who is an admin and can delete on top of create and update.
Defining policies and enforcing admin-only delete
Now how do we implement that? Well what we're going to do is create an identity holder over here and I'm just going to create a new class called IdentityData or IdentityConstants or whatever you really want to name it. Now first I'm going to add the constant that is the claim in my JSON Web Token and also I'm going to add a name to a policy in this case that is the AdminUserPolicy. Now these things are different: the claim is a JSON Web Token concern, the policy is an application concern. I can define my own policies based on claims or other things. To create a policy all I'm going to do is go all the way up here to AddAuthorization and I'm going to say AuthorizationOptions and say options.AddPolicy and I can create my own policy using a name. So I'm going to say IdentityData.AdminUserPolicyName and then what do I want? I want a policy using this PolicyBuilder that requires a claim and the claim I want is IdentityData.AdminUserClaimName and the value I want for this policy is "true." So now I have my policy registered. It's not used anywhere by default; we just have it now in the system and we can go ahead and just copy this and we can say that, you know what, Create and Update doesn't really need anything. I'll go all the way to Delete over here and say that actually I want this policy to be IdentityData.AdminUserPolicyName to delete and move in the system. So I'm going to run this and I'm going to use the previous token I have that has admin as false. So if I try to create a movie using that non-admin token then I can and I get my movie. But if I go to delete over here to delete that movie without being an admin what I'm going to get is 403 Forbidden, not 401 Unauthorized — Forbidden. You're forbidden to make this action. And that is because I just don't match the profile that the policy requires me to have, which is having the claim as true. Now if I go and I use this new token over here which is an admin then I can go here, replace this, send and now it is deleted and it no longer exists in the system. And that is it. Now you can go even deeper and have multiple policies and mix and match on different things in custom scenarios, but this is the most basic way of making a policy.
Custom claim-based attribute and minimal API differences
However some people prefer to validate based on a single claim without using a policy and one of the preferred ways is to actually create a custom attribute which validates claims. What I'm going to do is create a new attribute called RequiresClaimAttribute over here and I'm going to have that be an attribute used on classes or methods and it's going to extend Attribute and implement IAuthorizationFilter. If you needed an async version of this there's an IAsyncAuthorizationFilter. We don't need async in this case so I'm just going to use this and all you get is an OnAuthorization method. Now this is not usable with minimal APIs because of how the filtering works there. However for controllers this does work. If you want to have the equivalent of this in minimal APIs you have to use a minimal APIs endpoint filter. And now here all we need to pass down is a claim's name and the claim's value that we expect and then all we need to say in OnAuthorization is, hey, if this user doesn't have this claim with this value then say forbid or unauthorized depending on how the flow works in your application.
Debugging custom attribute and minimal API extensions; Swagger support
The reason why I say that is because in this example over here what we're going to say is actually Authorize first and then RequiresClaim second. And all we need here is actually the IdentityData.AdminUserClaimName and the value needs to be "true" and if we do that I'm going to put a breakpoint in the attribute and then go ahead and debug the application and again the same way as before I'm going to revert to the old token. In fact first I'm going to generate a movie so the movie is created, take that ID and then try to delete that movie with the valid token and let's see what happens. Oh, request comes in here. We have the context, we have everything and then the claim name is admin and the value we expect is true. Is that true? Well let's see ClaimsPrincipal over here and then we have all the claims and one of them is admin true so it will be validated so it will step out of it and the action will in fact happen. Okay if we use the old token and we try to do the exact same thing then we come in here but the user claim as you can see in the claims is actually false so it's going to fall in here, say Forbidden and then return 403 Forbidden because we are authenticated but not authorized. That's another way you can get clever and have a handy approach in claim-based authorization and you can take this in any direction you want really. Now if you were to be using minimal APIs all of those things are actually still valid. The difference would be that yes you can for example say MapPost and let's have a Movies endpoint over here and return Results.Ok for example. Now you can pass down an [Authorize] attribute over here and all of that will still work or RequiresClaim like that is just valid C# however in minimal APIs we actually prefer the extension method approach so we're going to say RequireAuthorization and you can create your own extension for the claims and everything but you can pass down like a policy over here. You can see all the parameters you can pass down multiple policy names as well but in this case if I just say IdentityData.AdminUserPolicyName this will be the same thing as having the attribute with the policy name. Now the last thing you might be interested is actually Swagger support because if I just go ahead and I run this API as it stands I can go in Swagger and even though I can see all my endpoints I can't see a way to actually authenticate with the token. So how do I do that? Well it's actually quite simple. All I'm going to do is create a new directory called Swagger and I'm going to bring two files in here. The first one is called ConfigureSwaggerOptions so we're going to contain all the Swagger-related options in here and this will need to implement the IConfigureOptions interface and then have the SwaggerGenOptions file in here implemented and we have the Configure method in here. And I'm not going to bore you with typing all of this but ultimately what you need is a SecurityDefinition and we say that this is a Bearer token, specify that it is located in the header Authorization, HTTP scheme Bearer, everything is here, same with the AddSecurityRequirement and really that is it from that front and then once you have that you actually need to register it in the Program.cs. So all the way up here we're going to go here and register the IConfigureOptions interface with its implementation and once that's in place all I'm going to do is just run it and now if we go back to Swagger as you can see I have this Authorize button which I can click and provide the JSON Web Token. So I'm going to go all the way back here, copy just the token, you don't need anything else and just paste it here, Authorize. And if I go to Create a movie for example and say Try it out, paste it and then Execute, no problem it all worked. It was created. If I wasn't authorized and I just click the padlock and log out and I try to do this again I'm going to get a 401. Hey, you cannot do this. So now we also have Swagger support. This is it for all the basics. You can take it in any direction you want from this point on. I do go way more in depth in my REST API calls on every single one of those avenues. However, you are now in the right track and you can take it everywhere you want from that point. Well that's all I have for you for this video. Thank you very much for watching. Special thanks to my patrons for making videos possible. If you want to support me you'll find the link in the description down below. Leave a like if you like this video, subscribe for more content like this and the bell as well, and I'll see you the next video. Keep coding.