Decode JWTs in C# for Authorization

Decode JWTs in C# for Authorization

There are two main steps in securing an application: authentication and authorization. Authentication is easy enough. A user enters a username and password (maybe even a second factor) to prove (authenticate) who they are. Authorization is a little less cut and dried. There are lots of factors that go into what an authenticated user is authorized to do. First, you need some information about the user that just authenticated.

Many modern web apps use JSON Web Tokens (JWTs) during the authentication process. These JWTs can also carry information that can be used for authorization decisions within your applications. In this tutorial, you will learn how to decode JWTs in C# and how to use information from a JWT to make authorization decisions in a .NET Core app.

TL;DR - How to Decode JWTs in C#

If you already have a JWT and you just want to know how to decode it in C#, here is the code you need:

var jwt = "(the JTW here)";
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(jwt);

// now do something with token.Claims, token.Audiences, etc.

Note that the code above just reads the information from the JWT. It does not validate the JWT to ensure that it hasn’t been forged or tampered with.

If you’re using OpenID Connect, there is actually a better way to access the information in the JWTs used in the authentication process, so read on!

Clone the JWT C# Project

To get started, clone or download the starter project from GitHub, and open the solution in Visual Studio 2019.

Next, click on the down arrow near the green start button and change startup action from IIS Express to DecodeJWTs.

Run with Kestrel

Now when you run the project, it will use the Kestrel web server instead of IIS Express. Before you run the project you will need to add some configuration settings from Okta, which you will do in a moment.

Set up an Authorization Server to Generate JWTs

While you could invent your own security system using JWTs, that would probably be a very bad idea. Security systems are complex, and there are many opportunities to get something wrong. So for this tutorial, you will use OpenID Connect for authentication so you won’t need to worry about generating and validating JWTs. You will just learn how to decode and use the JWTs that are used in the OpenID authentication handshake.

Before you begin, you’ll need a free Okta developer account. Install the Okta CLI and run okta register to sign up for a new account. If you already have an account, run okta login. Then, run okta apps create. Select the default app name, or change it as you see fit. Choose Web and press Enter.

Select Other. Then, change the Redirect URI to https://localhost:5001/authorization-code/callback and use https://localhost:5001/signout-callback-oidc for the Logout Redirect URI.

What does the Okta CLI do?

The Okta CLI will create an OIDC Web App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. You will see output like the following when it’s finished:

Okta application configuration has been written to: /path/to/app/.okta.env

Run cat .okta.env (or type .okta.env on Windows) to see the issuer and credentials for your app.

export OKTA_OAUTH2_ISSUER="https://dev-133337.okta.com/oauth2/default"
export OKTA_OAUTH2_CLIENT_ID="0oab8eb55Kb9jdMIr5d6"
export OKTA_OAUTH2_CLIENT_SECRET="NEVER-SHOW-SECRETS"

Your Okta domain is the first part of your issuer, before /oauth2/default.

NOTE: You can also use the Okta Admin Console to create your app. See Create a Web App for more information.

Add your Okta domain, client ID, and client secret to your appsettings.json file, like this:

"Okta": {
  "ClientId": "{OktaClientId}",
  "ClientSecret": "{OktaClientSecret}",
  "OktaDomain": "https://{yourOktaDomain}"
}

(Note that in order to keep these secrets out of source control you should move the ClientId and ClientSecret settings to your User Secrets file before you commit. You can skip this step for now since this is just a tutorial.)

Now run your project and make sure that you can sign in and out.

Review the OpenID Connect Authentication Middleware

Okta has some custom middleware that makes adding OpenID Connect authentication to your application really easy. But for this application, you will use the generic OpenID Connect middleware that comes with .NET Core so you can directly access the JWTs that are used in the authentication process.

In your Startup.cs file, you will see the configuration for the OpenID Connect middleware:

JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "OpenIdConnect";
})
.AddCookie()
.AddOpenIdConnect(options =>
{
    options.ClientId = Configuration.GetValue<string>("Okta:ClientId");
    options.ClientSecret = Configuration.GetValue<string>("Okta:ClientSecret");
    options.Authority = $"{Configuration.GetValue<string>("Okta:OktaDomain")}/oauth2/default";
    options.CallbackPath = "/authorization-code/callback";
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.UseTokenLifetime = false;
    options.GetClaimsFromUserInfoEndpoint = true;
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");

    options.TokenValidationParameters = new TokenValidationParameters
    {
      NameClaimType = "name",
      RoleClaimType = "role"
    };

    options.Events = new OpenIdConnectEvents()
    {
      OnUserInformationReceived = context =>
      {
        string rawAccessToken = context.ProtocolMessage.AccessToken;
        string rawIdToken = context.ProtocolMessage.IdToken;
        var handler = new JwtSecurityTokenHandler();
        var accessToken = handler.ReadJwtToken(rawAccessToken);
        var idToken = handler.ReadJwtToken(rawIdToken);

        // do something with the JWTs

        return Task.CompletedTask;
      },
    };
});

The code above configures your application to use Okta as an identity provider, using the settings you specified in appsettings.json. You will also notice some code that intercepts and decodes the two JWTs provided by Okta’s authorization server during the authentication process. The access token is always provided. It only contains essential information that identifies the user and grants access. Because you specified options.GetClaimsFromUserInfoEndpoint = true;, an identity token is also requested. This token will contain any additional information (claims) about the user that has been requested. In the example above, we requested the profile and email scopes, so the idToken.Claims collection will contain information such as the name and email address of the authenticated user.

Use Information in JWT for Authorization in C#

Now you’ll extract some information from the id token that you can use for authorization decisions.

You already learned how to decode the id token in the OnUserInformationReceived event of the OpenID Connect middleware. I have some good news for you. You don’t actually need to decode the JWT yourself because the middleware automatically decodes the id token and uses the information in it to populate the authenticated user’s claims.

If you set a breakpoint in the OnUserInformationReceived event and examine idToken.Claims and context.Principal.Claims, you will see that both properties contain a list of the exact same claims.

So how can you use the claims that are automatically extracted from the id token to make authorization decisions? I will share an example.

Assign User to Roles in C# Based on JWT Claims

Suppose you have a simple application that just has a couple of admin users which can be hard-coded into your application. In the OnUserInformationReceived event you could check if the authenticated user is one of the admins, and if so, assign the user to the Admin role, like this:

At the top of Startup.cs add

using System.Security.Claims;

Then replace the OnUserInformationReceived event with this code:

OnUserInformationReceived = context =>
{
    var admins = new string[]
    {
        "admin@company.com",
        "{your email address here}"
    };

    var additionalClaims = new List<Claim>();
    if (admins.Contains(context.Principal.FindFirstValue("email")))
    {
        additionalClaims.Add(new Claim("role", "Admin"));
    }

    var newIdentity = new ClaimsIdentity(context.Principal.Identity, additionalClaims, "pwd", "name", "role");
    context.Principal = new ClaimsPrincipal(newIdentity);

    return Task.CompletedTask;
},

In the code above you check to see if the user’s email address (that was provided in the JWT because we requested the email scope from the authorization server) is in the list of admins. If so, we recreate the ClaimsPrincipal, adding an additional claim for the Admin role.

The HomeController should already have an Admin method that is decorated with an [Authorize(Roles = "Admin")] attribute. Run the app and if you are signed in, sign out and sign in again to re-run the authentication middleware and create a new cookie. After that try to access the Admin page. If your email address is in the list of admins, you should be able to access the page. Otherwise, access should be denied.

Another way that you could use claims that are populated from a JWT for authorization decisions would be to write a custom authorization policy that contains custom authorization logic based on the information in context.User.Claims. I will leave that exercise for you.

Learn More About ASP.NET Core CRUD Apps and JWTs

Interested in learning more about ASP.NET Core, JWTs, or building secure applications with Okta? Check out our Product Documentation or any of these great resources:

If you have questions, feel free to post them below. Don’t miss any of our killer content by following us on Twitter and YouTube.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.