top of page

Storing a JWT inside an authentication cookie in Razor Pages

Writer's picture: Alejandro GaioAlejandro Gaio

Updated: Oct 24, 2022

TL; DR;

Assuming that you are familiar with cookies, json web tokens (JWT) and Razor Pages, I'll explain how a JWT obtained from an external API can be stored in an authentication cookie used by a web application and later be consumed for role authorization.


The full source code with comments can be located at: https://github.com/alegaio7/JwtInsideCookie


Introduction

JWTs encode user information in the form of claims. Claims are pieces of information that describe some property of a subject, for example a name, a role, a date of birth, and whatever a system may need to encode and pass along, and inside a JWT they are stored as key-value pairs.


The claims in the JWT can be used for both authentication (who the user is) and authorization (what the user can do).


In the examples shown in this post, authorization is enforced by checking claims in the JWT that relate to user roles.


The purpose of storing these tokens in cookies is to seize the fact that the latter are handled automatically by the browser (no code is required to send/receive them), thus providing an external place to store the JWT and delegating -indirectly- their storage to that component.


I call it an "external place" because another alternative is to store JWTs in the user's session in the backend, although this approach adds "state" to the naturally stateless paradigm of JWTs. If we think of an scalable solution, sessions should be avoided, or made available as a dependency service (which adds more complexity), and considered only if there's an specific requirement that depend upon them.


There is one more popular approach regarding JWT storage: the browser's Local Storage. Some pure javascript apps and frameworks use it (i.e. React), but that needs an additional step: attaching the JWT manually to every request we make to the backend.


As said before, cookies are managed automatically by the browser, and if cookies come and go on every request, so it does a JWT encoded inside.


Finally, there could be a security problem if JWTs are stored in Local Storage unencrypted. An attacker could gain access to them and make requests on user's behalf.


.Net core, by default, encrypts the authentication ticket that's stored inside the cookie. A cookie is not the same as the authentication properties (in the form of an AuthenticationTicket) that are encoded in it; cookies are just a mechanism for storing and sending bits of information that's specific for that server-browser link.

Cookie and authentication ticket
Cookie and authentication ticket

Note: There's post from Microsoft that explains the relationship between a cookie and the authentication ticket if you want to check it: https://support.microsoft.com/en-us/topic/understanding-the-forms-authentication-ticket-and-cookie-8ff63703-d7e0-5282-b8a6-9d516ee8a04d. Although it seems to be from some time ago, since it's focused on ASP.NET 2.0 and the .Net Framework, the concepts are mostly the same as explained here.


Overview of the requirement

I decided to write this post based on an application I was developing, which is a human-resources platform that allows users to upload PDF files and digitally sign them.


An early design decision was to split the solution into two separate components:

  • Web client: A Razor Pages application which uses cookies as the user authentication mechanism.

  • REST API: A .net core Api (MVC) application that will be consumed by the web client and, in the future, by other third parties. The API uses JWTs as the authentication mechanism.

For rest APIs, there are several ways for setting up an authentication mechanism like user/password, API keys, OpenID Connect and so on, but for simplicity purposes I decided to go with the username/password approach.


Authentication flow between the UI and API components
Authentication flow between the UI and API components

Explanation

  1. The web application displays a login page and gets the user's credentials.

  2. In a POST request, credentials are sent to the backend (PageModel).

  3. The backend builds a request and calls the API using GET to the /api/auth/gettoken endpoint. The request contains the username and password collected before.

  4. The request is handled by the Auth controller in the API component. This controller validates de credentials against a repository (usually a database, but in the code examples the user information is hardcoded for simplicity). If successful, the controller builds a list of claims that represent the user's identity. A special custom claim is also added, represented by the class UserState, which contains additional information needed by the solution (i.e. the roles the user has in the platform).

  5. The API builds a json web token with the standard claims (username, email, full name), and our UserState as a custom claim.

  6. The API returns the signed JWT to the caller in a AuthResult class. This class has just 2 properties: the JWT in string format, and its expiration date.

  7. The PageModel of the login page receives this AuthResult and validates the JWT using the helper class JwtSecurityTokenHandler, which is part of the System.IdentityModel.Tokens.Jwt package. If validation is successful, a ClaimsPrincipal class is returned, which is a generic representation of the security context of our user. This ClaimsPrincipal class contains the same claims that were encoded initially in the API's Auth controller.

  8. A dictionary of authentication properties is created (AuthenticationProperties class), which contains a single authentication token (key-value pair) corresponding to the JWT and its string representation.

  9. The claims principal, along with the authentication properties are used to call HttpContext.SignInAsync, using the "cookies" scheme.

  10. During this sign in process, the cookies middleware calls a custom class of our own (JwtAuthTicketFormat) with the AuthenticationTicket. This class is responsible of protecting (encrypting) the ticket when going out to the browser, and unprotecting it (decrypting) when receiving it back from the browser.

  11. On every request from the browser to the backend, the cookie is attached automatically, but before this request arrives to the corresponding endpoint a custom middleware extracts the JWT from the AuthenticationTicket inside the cookie, decrypts it, and builds a custom ClaimsPrincipal (ApiClaimsPrincipal) with all the standard and custom claims attached to it.

  12. Finally, the User property of the HttpContext is replaced by this ApiClaimsPrincipal, so it can be accessible to every middleware in the pipeline including the final endpoint. This also means that we can check for user roles whenever we need to.


The code, step by step

Since this post is about storing JWTs in cookies, we're not going to explain the API part of user validation and JWT generation, only the consumer-side of the solution will be detailed.


Web application

This section will explain the flow by showing some code and references to the points described earlier in the Explanation section.


The first important piece of code in this flow is located in Startup.ConfigureServices method. First, a data protector is created using our application's name:

var dpp = DataProtectionProvider.Create("JwtInsideCookie");

Data protectors are the .net core, platform-agnostic way to delegate secrets management (key encryption, rotation and so on) in a transparent and simple way, so the developer can focus on code instead of the internal implementations and the security implications. For more information, check https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-6.0


Next, the cookies middleware is added to the service collection:

services
.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options => {
    options.ExpireTimeSpan = TimeSpan.FromMinutes(20);
    options.SlidingExpiration = true;
    options.TicketDataFormat = new JwtAuthTicketFormat(
        JwtOnCookieHelper.TokenValidationParameters,
        TicketSerializer.Default, 
        dpp.CreateProtector("CookieEncryption")
    );
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.AccessDeniedPath = options.LoginPath;
    options.ReturnUrlParameter = "returnUrl";
});

Besides adding the cookies middleware, this code configures the TicketDataFormat option. In our example, TicketDataFormat is implemented in a custom class (JwtAuthTicketFormat) whose responsibility is to protect and unprotect the authentication ticket stored inside the cookie.


The custom class is initialized using:

  • A custom set of validation parameters, defined in a helper class JwtOnCookieHelper

  • A default ticket serializer provided by .net core which serializes and deserializes instances of authentication tickets

  • A data protector created using a data protector provider we created earlier using DataProtectorProvider.Create.

So, when a cookie is built, an AuthenticationTicket instance is serialized to an array of bytes by the TicketSerializer, and then it is protected (encrypted) by the IDataProtector that is created by the IDataProtectionProvider (dpp class).

Sounds a bit complicated to grasp all these concepts and how they tie together, but it has a logic behind: some data (authentication data) must be first converted to a serializable format (TicketSerializer -> byte array) and then protected in order to leave the service and be stored safely in the browser as a cookie (IDataProtector -> encrypted bytes).

The validation parameters that the JwtOnCookieHelper class returns look like this:

public static TokenValidationParameters TokenValidationParameters { get; } = new TokenValidationParameters() {
    ValidateIssuerSigningKey = false,
    ValidateAudience = true,
    ValidAudience = "http://Api",
    ValidateIssuer = true,
    ValidIssuer = "http://Api",
    ValidateLifetime = true,
    SignatureValidator = delegate (string token,
        TokenValidationParameters parameters) // needed since token 
            was created in another service:
            https://github.com/aspnet/Security/issues/1741
    {
        return new JwtSecurityToken(token);
    }
};

The parameters in bold should match between the consumer (Razor Pages) and the token provider (REST Api).


Another important piece of code is used in the Configure method of Startup.cs, which adds two middleware modules written by ourselves.


UserStateExtractorMiddleware is responsible of extracting the UserState from the JWT inside the cookie, and UnauthorizedJwtRedirectMiddleware is responsible for redirecting the user to the login page when an Unauthorized (401) response is received from the API:


Custom middleware
Custom middleware

When the login screen is presented and the user enters her username, password, and then hit the sign in button, the page backend (Login's PageModel) is invoked with a POST request. Here's the code of OnPostAsync:

public async Task<IActionResult> OnPostAsync([FromBody] InputModel input, string returnUrl = null)
{
    returnUrl ??= Url.Content("~/");
    var validations = new Dictionary<string, string>();

    try
    {
        if (!ModelState.IsValid)
            return JsonResult(false, 
                validations, 
                "Invalid data. Please check the form values.");

        var client = GetClient(HttpContext, true);
        // pass true to dontAuthenticate to avoid jwt auth, 
        // since this is for getting a token
        var authResult = await GetContents<AuthResult>(
            HttpContext,
            $"api/auth/gettoken?userName={input.username}&
                password={input.password}",
            dontAuthenticate: true);

        var userState = await 
            JwtOnCookieHelper.SignInUsingJwt(authResult, 
            HttpContext);

        return new OkObjectResult(BoxContents(true, 
            new { returnUrl = returnUrl }));
    }
    catch (UIGenericHttpException ex)
    {
        // log ex.
        return StatusCode((int)ex.StatusCode, BoxError(ex));
    }
}

There are some helper methods in there, but they're used to obtain an HttpClient from the services container through dependency injection (GetClient) and to build the request for the API response (GetContents).

The result of GetContents is an AuthResult class which contains the JWT token obtained from the API.


The AuthResult class looks like this:

AuthResult containing the JWT token
AuthResult containing the JWT token

The other highlighted text is the method SignInUsingJwt, which is in a helper class.


This method takes the AuthResult class and initiates the sign in process with using the cookie:

public static async Task<UserState> SignInUsingJwt(AuthResult authResult, HttpContext context) {
    var th = new JwtSecurityTokenHandler();
    th.MapInboundClaims = false;

    var claimsPrincipal = th.ValidateToken(authResult.token, 
        JwtOnCookieHelper.TokenValidationParameters, out 
        SecurityToken validatedToken);

    if (validatedToken.ValidFrom > DateTime.UtcNow || 
        validatedToken.ValidTo < DateTime.UtcNow)
        throw new Exception("Invalid token");

    // puts the JWT token in an authentication cookie. 
    // The cookie will be encrypted using data protection api
    var ap = new AuthenticationProperties() { IsPersistent = true };
    ap.StoreTokens(new[] {
        new AuthenticationToken() { 
        Name = JwtAuthTicketFormat.TokenConstants.TokenName, 
        Value = authResult.token
        }
    });

    [... code omitted for brevity ...]

    await context.SignInAsync(
        CookieAuthenticationDefaults.AuthenticationScheme,
        claimsPrincipal,
        ap);
}

This method creates a JwtSecurityTokenHandler, which as stated earlier, is in the System.IdentityModel.Tokens.Jwt assembly.


Then the ValidateToken method of this class is used. This method will validate the JWT against a set of parameters we pass to it (JwtOnCookieHelper.TokenValidationParameters) and returns a ClaimsPrincipal.


After that we perform a data range validation to check whether the token presented is still valid or an intent to use an expired token was made.

If everything goes fine up to this point, a dictionary of authentication properties is constructed by using the class AuthenticationProperties. This class, along with the ClaimsPrincipal are the core of what will be inside the cookie.


We say AuthenticationProperties is a dictionary because it allows storing several tokens by using its StoreTokens method, although in this example we're storing only the JWT obtained from the API.


The last part is what signs the user in with the web application.


HttpContext.SignInAsync is an extension method of the Microsoft.AspNetCore.Authentication.Abstractions package, and by using the "cookies" authentication scheme (as stated by the constant CookieAuthenticationDefaults.AuthenticationScheme), we're telling the cookies middleware that we want to use it for user authentication.

The cookies middleware does a call to our custom class JwtAuthTicketFormat. If you recall from before, we use the Protect and Unprotect methods of this class to encrypt and decrypt the ticket inside the cookie, since it implements the ISecureDataFormat<AuthenticationTicket> interface.


This is what the Protect method receives when called by the sign in process:

Protecting the authentication ticket
Protecting the authentication ticket

Note: The ticketSerializer class that is depicted in the image is just TicketSerializer.Default, a default implementation provided by .net core that handles the conversion of AuthenticationTickets to byte arrays. As you can see, the authentication ticket goes by 3 transformations before it is returned for storing in the cookie:


  1. It is serialized as a byte array.

  2. The array is encrypted using the IDataProtector.Protect method.

  3. It is encoded as a Base64 string.

When all the work is done, the cookie is sent back to the browser as a header in the response.


Whenever a request is made back from the browser to the server, an inverse pathway is executed: the JwtAuthTicketFormat.Unprotect method is called to:


  1. Decode the Base64 string into a byte array.

  2. The IDataProtector.Unprotect method decrypts the byte array.

  3. The array is converted (deserialized) to an instance of AuthenticationTicket.

If any of this procedures fail, we can assume that the ticket is invalid and we should redirect the user to perform a sign in action again.

We have the AuthenticationTicket after a browser request, now what?

The ticket signals the framework that the user is authenticated, but we want to make use of her roles and other properties that were encoded in the token.


This is where UserStateExtractorMiddleware comes in.

We need to get the custom UserState claim encoded by the API in first place, which contains the user roles we defined for our application.


Although roles can be defined using standard claims, we use the USerState class in order to provide more flexibility. For example, we can encode the name of the company the user belongs to, user's group memberships, or whatever we want.


We only have to take into account that every piece of information that we add to the JWT will make is larger, and this raises 2 concerns:

  1. We don't want it to be too large. Recall that every browser request includes all the cookies from the domain.

  2. Cookies have a theoretical limit of around 4K (may vary from browser to browser), and we don't want to play with limits here.

The code in UserStateExtractorMiddleware is the following:

if (context.User?.Identity != null && 
    context.User.Identity.IsAuthenticated && 
    context.User.Claims != null)
{
    var currentPrincipal = context.User;
    var userStateJson = currentPrincipal.Claims.FirstOrDefault(x => 
        x.Type == "UserState")?.Value;
    UserState userState = default;
    if (!string.IsNullOrEmpty(userStateJson))
    {
        userState = JsonSerializer.Deserialize<UserState>
            (userStateJson);
        if (userState != null)
        {
            userState.UserName = 
                currentPrincipal.Claims.FirstOrDefault(
                x => x.Type == JwtRegisteredClaimNames.Sub)?.Value;
            userState.Name = currentPrincipal.Claims.FirstOrDefault(
                x => x.Type == JwtRegisteredClaimNames.Name)?.Value;
            userState.Email = currentPrincipal.Claims.FirstOrDefault(
                x => x.Type == JwtRegisteredClaimNames.Email)?.Value;


            var userIdentity = new ClaimsIdentity(
                CookieAuthenticationDefaults.AuthenticationScheme);
            foreach (var c in currentPrincipal.Claims)
                userIdentity.AddClaim(c);

            var cp = new ApiClaimsPrincipal(userIdentity);
            cp.UserState = userState;
            context.User = cp;
        }
    }
}

This middleware first checks that there's an authenticated user in the context, and that the user has claims attached.


If the check passes, an intent to extract the "UserState" custom claim is done, and if successful, the claim's contents are deserialized into a new UserSate object.


Then, a transcription of claims from the token to this UserState class is made.


The last part builds a new identity based on claims, and uses it to create a new ApiClaimsPrincipal (which is no more than a class derived from ClaimsPrincipal with an additional UserState property), and finally this principal is used to replace the one in the HttpContext.User property, so it can be consumed by all components and endpoints thereafter.


Consuming the UserState in a page

The Index.chtml shows how the user roles can be used to enable or disable parts of the user interface:

Use of roles to customize the UI
Use of roles to customize the UI

The same logic can be used in a PageModel endpoint. For example, suppose that the IndexModel (the PageModel of the Index.cshtml page), had to check whether the user has certain role before executing a task:

public async Task OnGet() 
{
    var apiUser = HttpContext.User as ApiClaimsPrincipal;
    if (apiUser != null && 
        apiUser.HasRole(RoleDTO.AdministratorRole))
    {
        // do something
    }
}

Wrapping all up

The "understating barrier" that must be broken is the relationship between the different classes that make up the authentication process, and once you do it, everything comes clearer.

At first, it's quite blurry because one component uses cookies (the UI) and the other uses JTW (the API), and we're kind of mixing both worlds, but keep in mind this graphic for a clearer reference:


Sign in and browser request events
Sign in and browser request events

So that's it. Please leave a comment if you find this useful, or even confusing!


In the latter case I'll try to do my best to make things clearer, or at least reply with an insightful answer.



174 views0 comments

Comments


©2022 by Alejandro Gaio.

bottom of page