Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • JavaScript • Angular
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

Role based JWT Tokens in ASP.NET Core APIs


:P
On this page:
Edit this Post

Authentication and Authorization in ASP.NET Core continues to be the most fiddly component for configuration. It seems almost on every app I run into some sort of sticking point with Auth. Four versions in have brought three different authentication implementations and feature churn has also left a wave of out of date information in its wake. Today I got stuck in one of those Groundhog Day loops looking at outdated information with JWT Tokens for a Web API with Role based authorization.

The current iteration of JWT Token setup in ASP.NET Core actually works very well, as long as you get the right incantations of config settings strung together. Part of the problem with Auth configuration is that most of settings have nothing to do with the problem at hand and deal with protocol ceremony. For example, setting Issuer and Audience seems totally arcane but it's part of the requirements for JWT Tokens and do need to be configured. Luckily there are only a few of those settings that are actually required and most of it is boilerplate.

I've not found this information all in one place, and today I barked up the wrong tree for a couple of hours in regards to Role authorization with JWT Tokens, where my app would validate non-role Authorizations, but not role based ones. So, now that I managed to get it all working, I'm writing it down so I can find it next time around.

In this post I specifically talk about:

  • Authentication for an ASP.NET Core Web API
  • Using JWT Tokens
  • Using Role Based Authorization
  • Using only ASP.NET's low level Auth features - not using ASP.NET Core Identity

Configuration

Authentication and Authorization are provided as Middleware in ASP.NET Core and is traditional, you have to configure them in .ConfigureServices() and connect the middleware in .Configure().

Setting up JWT Authentication and Authorization

First step is to configure Authentication in Startup.ConfigureServices(). This is used to configure the JWT Token set up and add the required components to ASP.NET's processing pipeline:

// in ConfigureServices()

// config shown  for reference values
config.JwtToken.Issuer = "https://mysite.com";
config.JwtToken.Audience = "https://mysite.com";
config.JwtToken.SigningKey = "12345@4321";  //  some long id

// Configure Authentication
services.AddAuthentication( auth=>
{
    auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.SaveToken = true;
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuer = true,
        ValidIssuer = config.JwtToken.Issuer,
        ValidateAudience = true,
        ValidAudience = config.JwtToken.Audience,
        ValidateIssuerSigningKey = true,
        IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(config.JwtToken.SigningKey))
    };
}

JWT Authentication has a ton of settings, most of which are sufficiently cryptic that I'm pretty much just going to cut and paste them and fill in the values. Suffice it to say most of these concern setting up the protocol, and token wrapper. Typically I store those values in my app's configuration settings so it gets pulled in via .NET's configuration provider and .config is that particular configuration instance.

There's nothing Role specific in this global configuration. All the role based related configuration happens when creating a token later on in the Authenticate endpoint.

How Tokens and Hashing work

Before moving on here, let's review how token based authentication works and how these setup values fit into this scheme.

The setup values above configure the token's common values and key used to sign the token. They provide identification markers to ensure that the token generated is unique. I consider these values a base token wrapper, and when you create a token you'll add your custom, application specific claims to the token typically right after you authenticate a user and provide the token back to the user or auth flow as part of a Web request.

The IssuerSigningKey is the most important part of this configuration, and it's used to hash the final token with the wrapper plus any claims added. The hash is used to validate the token's authenticity. Note while the generated token is encoded as base64, it is not by itself secure and the content can be decoded even on the client. To wit, you can take any JWT token and paste it into JWT.io and decode the token's content.

The hash ensures that the token cannot be changed without breaking the token validity. When the token is sent with a request, it's validated by ASP.NET Core's JWToken middleware which first validates the hash against the token data and then applies the authentication/authorization based on the contained well-known authorization values. If the token has been changed by the client or some other entity in any way the hash won't validate and it's rejected outright. After that the username and role matching is applied in the authorization part of the middleware pipeline.

Adding the Auth Middleware

Next we need to add the actual middleware for .UseAuthentication() and app.UseAuthorization() in Startup.Configure:

// in Startup.Configure()
app.UseHttpsRedirection();
app.UseRouting();

// *** These are the important ones - note order matters ***
app.UseAuthentication();
app.UseAuthorization();

app.UseStatusCodePages();
//app.UseDefaultFiles(); // so index.html is not required
//app.UseStaticFiles();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();
});

Note that order matters for Authentication and Authorization. These two need to be injected after Routing but before any HTTP output generating middleware, most importantly before app.UseEndpoints().

Authenticating Users using an Web API Endpoint

Next we need to authenticate a user within the application by asking for credentials, and then generating a token and returning it to the API client.

This likely happens a Controller Action Method or Middleware Endpoint Handler. Here's what this looks like using a Controller Action Method:

[AllowAnonymous]
[HttpPost]
[Route("authenticate")]
public object Authenticate(AuthenticateRequestModel loginUser)
{
	// My application logic to validate the user
	// returns a user entity with Roles collection
    var bus = new AccountBusiness();
    var user = bus.AuthenticateUser(loginUser.Username, loginUser.Password);
    if (user == null)
        throw new ApiException("Invalid Login Credentials: " + bus.ErrorMessage, 401);

    var claims = new List<Claim>();
    claims.Add(new Claim("username",loginUser.Username));
    claims.Add(new Claim("displayname",loginUser.Name));
    
    // Add roles as multiple claims
    foreach(var role in user.Roles) 
    {
        claims.Add(new Claim(ClaimTypes.Role, role.Name));
    }
    // Optionally add other app specific claims as needed
    claims.Add(new Claim("UserState", UserState.ToString()));

    // create a new token with token helper and add our claim
    // from `Westwind.AspNetCore`  NuGet Package
    var token = JwtHelper.GetJwtToken(
        loginUser.Username,
        Configuration.JwtToken.SigningKey,
        Configuration.JwtToken.Issuer,
        Configuration.JwtToken.Audience,
        TimeSpan.FromMinutes(Configuration.JwtToken.TokenTimeoutMinutes),
        claims.ToArray());
    
    return new
    {
        token = new JwtSecurityTokenHandler().WriteToken(token),
        expires = token.ValidTo
    };
}

I'm using a JWTHelper class to actually generate a token so I don't have to remember this repetitive ‘ceremony’ in each application from the JwtHelper class dependency. The code creates the token and extracts a string from it that's ready to be returned as a bearer token value. Here's the relevant code:

public class JwtHelper
{
    public static JwtSecurityToken GetJwtToken(
        string username,
        string signingKey,
        string issuer,
        string audience,
        TimeSpan expiration,
        Claim[] additionalClaims = null)
    {
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub,username),
            // this guarantees the token is unique
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        if (additionalClaims is object)
        {
            var claimList = new List<Claim>(claims);
            claimList.AddRange(additionalClaims);
            claims = claimList.ToArray();
        }

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(uniqueKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        return new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            expires: DateTime.UtcNow.Add(expiration),
            claims: claims,
            signingCredentials: creds
        );
    }
}

The Authenticate() controller code first uses an application specific business object to validate the user passed in as part of the API call (or Login form if you're doing HTML forms). If the user is valid, I create new claims that are packaged into the token.

The tokens include the username and roles which is what's required for ASP.NET Core's authorization to work. I then can add some additional application specific claims if necessary like the display name and a custom UserState object in the example above. These claims travel with the token and can be retrieved later without having to access a backend to retrieve them again.

Finally the token is generated using JwtHelper.GetJwtToken() with the user id as the key a signing key, some site specific state and the actual claims. Finally you can turn the token into a string:

var tokenString = new JwtSecurityTokenHandler().WriteToken(token);

which can then be used by the client as a Bearer token.

Note that the Authentication method needs to be anonymously accessible if the AccountController is otherwise set to [Authorize]. make sure that the Authenticate() method has an [AllowAnonymous] attribute.

Claims and Roles, Roles, Roles

ASP.NET Core uses Claims for authentication. Claims are pieces of data that you can store in the token that are carried with it and can be read from the token. For authorization Roles can be applied as Claims.

The correct syntax for adding Roles that ASP.NET Core recognizes for Authorization is in .NET Core 3.1 and 5.x is by adding multiple claims for each role:

// Add roles as multiple claims
foreach(var role in user.Roles) 
{
	claims.Add(new Claim(ClaimTypes.Role, role.Name));
	
	// these also work - and reduce token size
	// claims.Add(new Claim("roles", role.Name));
	// claims.Add(new Claim("role", role.Name));
}

Invalidating a Token

Accessing the JWT Token Generation API

So at this point I have an Authenticate API endpoint that I can retrieve a token from. Here's what this specific request looks like:

Username and password are passed in, and the token along with an expiration time is passed back. You can check out this token and see what it generated at https://jwt.io:

Notice that the token is easily decoded by an external tool, totally unrelated to my application. This means the contained token data is not secure from prying eyes. However, the values in that token can't be changed and provided to the server application unless the data is signed by the original Signing Key. This prevents the token from being tampered with.

Once the token's been generated and sent to the client, the client can now use it in subsequent requests to add the appropriate Authorization header:

Authorization: Bearer 1235*53213...

to outgoing requests.

Securing the API

What's left now is to selectively or restrictively limit access to the API(s) by adding [Authorize] attributes either on controllers or endpoint methods.

I can use one of the following or no attributes at all (for open access):

  • Plain [Authorize] to let any authenticated user in
  • Role based [Authorize(Roles = "Administrator,ReportUser")]
  • Anonymous [AllowAnonymous]

Note the attributes can be set on a Controller class, or Action method and they work hierarchically from the top down, so a class attribute applies to all action methods. This is where [AllowAnonymous] comes in handy to override the one or two requests that might need open access (like Authenticate() or Logout()).

To set up authorization for any authorized user, just use [Authorize]:

[Authorize]   // just require ANY authentication
[Route("/api/v1/lookups")]
public class IdLookupController : BaseApiController

In this scenario you likely need to do some additional validation of the user to ensure you have the right user for specific operations.

To set up up Role specific restrictions you can use the Roles parameter:

[Authorize(Roles = "Administrator")]
[HttpPost]
[Route("customers")]
public async Task<SaveResponseModel> SaveCustomer(IdvCustomer model)

Now only those that are part of the Administrator group have access. The List can use multiple roles using a comma delimited list ie. "Administrator, ReportUser".

Accessing Secured Endpoints with the Bearer Token

Now that the API is secured we have to pass the Bearer token with each request to authenticate. It looks like this:

And voila - I can now access the Administrator group protected POST operation.

And that completes the circle…

Logging Out JWT Tokens: Fogettaboutit!

I got several questions regarding out with a JWT token and the short answer to that is:

  • You can't ‘log out’ with a JWT Token
  • A token once generated is valid until it expires
  • The only thing that expires a JWT token is its expiration time

You can't log out with only a JWT token. Unlike a cookie or session there's nothing to kill or remove because JWT tokens are stateless.

JWTs are self contained and there's nothing backing them but the data they contain. So the server has no idea beyond the validity of the token and its expiration time whether its valid or not.

Note that if you're writing HTML based applications, you can use a cookie or some local storage to hold the Token and log out the user by removing it. You can clear the cookie or client side API applications can remove the token from the client, and that effectively logs out the application. But even though these ‘wrappers’ may clear the token for the application, the actual token remains valid until expiration, if the token is somehow peeled out of the application wrapper or cookie.

Have to log out anyway: A secondary layer

If you really, really need to be able to log out, you can wrap a secondary layer around the JWT token in your token validation logic. For example, you could add a record into a DB or other storage that holds the token and access status. When the token is validated, you then also check for the access status in the DB. To ‘log out’ you can remove the record or mark it as logged out to disallow access even if the token has not expired. It's ugly and requires some stateful storage, which kind of defeats the whole idea of JWT Tokens in the first place, but it works and is not that difficult to set up.

Keep the Timeout Short

If you are concerned about Token lifetime, the key is to keep the token timeout short. This means tokens have a limited lifespan and are unlikely to be useful to anything but live attacks.

Timeouts can be a pain for client applications since they now have to check for the token timeouts. You can ease that pain by having returning useful error information on a timeout (instead of just a 401), and have an easy way to create a new token so that client applications can automate that process.

Summary

Authentication and Authorization in ASP.NET Core has gotten a lot simpler in recent versions, but finding the right documentation for setting all the dials for JWT Token Authentication is still not very obvious. There's a lot of information about authentication and it's easy to get lost in the docs and end up on outdated information, because the behavior of Authentication has changed significantly throughout ASP.NET Core versions. If you're looking up additional information make sure it's for version 3.1 and later which is the latest as of now.

In this post I've addressed what works for 3.1 and 5.0. Mercifully 5.0 saw no further breaking changes to the Authentication/Authorization APIs.

As is often the case I'm writing this down for my own peace of mind so I have all the information in one place. Hopefully some of you'll find this useful as well.

this post created and published with the Markdown Monster Editor
Posted in ASP.NET Core  Security  

The Voices of Reason


 

Travis Laborde
March 10, 2021

# re: Role based JWT Tokens in ASP.NET Core

Great article as always. Good timing too for my uses 😃


Oisin Grehan
March 10, 2021

# re: Role based JWT Tokens in ASP.NET Core

Great write up, Rick -- I went through similar hell recently with azure AD app registrations, scopes, audiences, issuers and application vs delegated permissions. One thing I might point out that will probably drive you nuts is that it's only the UseAuthorization() call that has to appear after UseRouting() and before MapEndpoints(). The UseAuthentication() call can appear before UseRouting() (though grouping authz/authn together makes perfect sense to me from an organizational standpoint!)


Rick Strahl
March 10, 2021

# re: Role based JWT Tokens in ASP.NET Core

@Osin - It's possible this has changed in recent versions. I had an open issue for this a while back but AFAIK that never was fully addressed. At the time the order of UseAuthentication() and UseAuthorization() was also important.

Ever since I've just made sure not to do it any differently because the behavior was subtly different.


onnorh
March 11, 2021

# re: Role based JWT Tokens in ASP.NET Core

How would expiration of the token be handled by the calling client and the api controller?


Paul Speranza
March 11, 2021

# re: Role based JWT Tokens in ASP.NET Core

Yep, I have done the auth Ground Hog Day shuffle.


Branislav Petrović
March 11, 2021

# re: Role based JWT Tokens in ASP.NET Core

Great and concise article, nicely written!

Thanks Rick!


Nicholas Paldino
March 11, 2021

# re: Role based JWT Tokens in ASP.NET Core

It might be splitting hairs, but consider a scenario where the token key for the mechanism is compromised.

In this scenario, someone could fabricate any claim/role they want.

If you wanted an added layer of security, would it be preferable to fetch the roles given the user ID?

Even if the signing token is compromised, attackers would still have to find a valid user with the appropriate roles to gain the desired access.


Rick Strahl
March 11, 2021

# re: Role based JWT Tokens in ASP.NET Core

@Nicholas - if I understand JWT Tokens correctly the hash is what makes this work. You can read the data, but you can't change it because you'd need the signing key.

If that wasn't the case, JWT Tokens would be pointless and we might as well pass a JSON string around 😃. The point is good though - you definitely don't want to pass information that can compromise the application in a token as it is readable if the token is captured - either by cookies or bearer tokens.


Szilard
March 16, 2021

# re: Role based JWT Tokens in ASP.NET Core

Thanks Rick!

Is the JwtHelper class part of a NuGet package or is it custom code?

I installed Microsoft.AspNetCore.Authentication.JwtBearer (which is required for AddJwtBearer), but it doesn't contain the JwtHelper class.

Thanks


Rick Strahl
March 16, 2021

# re: Role based JWT Tokens in ASP.NET Core

It's a custom class that I have in my AspNet utility library in Westwind.AspNetCore Nuget package. Source code here:

JwtHelper Class


Christof Jans
April 18, 2021

# re: Role based JWT Tokens in ASP.NET Core

Great post. I still have a question :

How do you invalidate a token ? How does a user log out ?


Rick Strahl
April 20, 2021

# re: Role based JWT Tokens in ASP.NET Core

@Christof - you can't 'logout' with JWT Tokens. A token once generated is valid until it expires, so from the server side you can't invalidate a token, unless you add some secondary authentication layer around it: For example keeping track of tokens issues in a Db or even memory and looking up the token when it's validated to see if it's still valid.

If you use the token in a client application you can remove a login cookie, or the API token stored on the client and that effectively logs the client out, but in theory the token is still valid and if used will still work.


Josh
May 17, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

So I'm getting really close on this, but for some reason I still can't hit my endpoint if it has the [Authorize(Roles = MyRoles.Administrator)] attribute. I think that I have set it up properly... When I decode my token, I get the following payload:

{
  "sub": "administrator",
  "jti": "ba9f388a-591b-4be1-ae9a-a3aa52b90b84",
  "username": "administrator",
  "userid": "34613a8d-40eb-469e-a37a-98060ee4bf0d",
  "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Administrator",
  "exp": 1621293379,
  "iss": "https://localhost:44308/",
  "aud": "https://localhost:8090/"
}

Any ideas why I would get the proper payload but still cannot hit my endpoint (I do hit the endpoint if I remove the attribute)? My biggest suspicion is my Issuer and Audience strings. I have tried simply https://mysite.com in case just anything would work, and I've tried https://localhost:44308/ for issuer and https://localhost:8090/ since these are the domains of my web API and my UI project.


Rick Strahl
May 17, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

The fact that you can decode the token doesn't mean that it's valid, because the portion that you are displaying is not encoded. The key for ASP.NET validation is the key hash that is encoded with the application level key that is used to validate the data and ensure its integrity with the server key and timeout.

To test - make sure it works without the roles first. Remove the role check and just check user access. If that works, then you can look into why it doesn't work with the role.


OzBob
May 26, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

Once the token expires, how do you refresh it?


Rick Strahl
May 27, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

@OzBob - you can't refresh a token because the thing is basically a snapshot with the expiration part of the signature. Once it expires it's done.

I think the way to deal with this is to expect a client application to fail with an invalid token and have custom functionality around that. A couple of things I've done for this:

  • Add a custom RefreshToken endpoint - you have to be authenticated and it'll issue you a new token that resets the timeout.
  • When the token gets close to expiring add a new Token to the outgoing request say in an HTTP header that the client app can check for to pick up a new token for extension.

Neither of these is a built-in feature, so this relies on application specific logic.


Thomas Haukland
June 23, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

Thanks, this worked like a charm!

I scrapped my identityserver attempt(which worked, but token endpoint required "client_id" which the old clients didnt provide(and isnt required as per the spec...))

One little thing: I think you have a typo in your helper class, replace "uniqueKey" with "signingKey" and it works fine


Wynand Murray
June 28, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

Yet another simple, clear example. Thank you, works like a charm.


Nash
July 22, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

Is there a way to package (nuget?) the jwt validation so that the same policies can be used in individual APIs without rewriting all of the policies?


Rick Strahl
July 22, 2021

# re: Role based JWT Tokens in ASP.NET Core APIs

@Nash - sure you can create a class that assigns all the default functions etc. and then package that up as a NuGet package.

But - these are configuration values, and they are meant to be customized for each application, so I'm not sure that that is really useful other than saving some typing or cut and paste. Once you start account for generic behavior I don't think there's a good way to abstract the abstraction 😄


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2021