Generating and using a JWT in .NET

2021-Jun-28

Introduction

ASP.NET Core Identity uses cookie authentication out of the box. However the cookies are encrypted in a way that makes them hard to share between multiple processes.

This article describes using a self-generated JWT for authentication, which allows multiple processes in an application to authenticate a logged-in user.

The application is deployed here: https://jwtauth.rgbco.uk/

The GitHub repository is here: https://github.com/.../JwtAuth

Some alternatives are first noted:

Then using a JWT and some notes are described:

Sharing Cookie Authentication

The built-in cookie authentication uses the data-protection API to encrypt cookies. The API provides options for overriding how keys are persisted, which makes it possible to have multiple servers share the same encryption keys.

The simplest of these is to store the keys in a common folder that all servers have access to:


    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDataProtection()
            .PersistKeysToFileSystem(new DirectoryInfo("s:\\shared_folder"));
    

However, it is not always practical to share the keys between multiple processes/servers.

Using OIDC

OpenID Connect (OIDC) is becoming the standard for authenticating users. The .NET Core framework provides extension methods to make the code simple to add for clients.

If you are using an external OIDC provider, then you are relieved of the (significant) headache of managing users. Or at least of managing storing their credentials. However, you are now tied to that external provider (e.g., Azure Active Directory, Auth0, ...).

If you are creating your own OIDC solution (using e.g., Identity Server 4/Duende, OpenIddict ),then you are tied to maintaining it. The cost of maintaining such a solution might be significant.

Using a JWT

This example application demonstrates generating a JWT, storing it in a cookie, and validating the JWT in a separate process.

Storing Identity

The identity pages are scaffolded using the ASP.NET Core Identity templates. The identities are stored using Entity Framework. For this simple example the FileContextCore provider is used:

    
    services.AddDbContext(options =>
        options.UseFileContextDatabase(location: context.HostingEnvironment.AppFolder("auth_ex_security_db")));
    

Example Accounts

At startup, two example accounts are created, and buttons are provided in the UI to allow these example users to login:

Example Logins

Generate a JWT

In order to generate a JWT, we need a key. Specifically, we want an RSA key so that we can share the public portion of the key with other processes, but keep the private key secret to the security module. At startup, the key is generated and stored in the (file) database:

        
    var newKey = RSA.Create(4096);
    db.RsaKeys.Add(newKey.ToXmlString(true));
        
    

A JwtSignInHandler is created and registered at startup:

        
    services.AddAuthentication()
        .AddScheme<AuthenticationSchemeOptions, JwtSignInHandler>(IdentityConstants.ApplicationScheme, o => { })
        
    

When the user is signed in (via the SignInManager), the key is retrieved and a JWT is created:

        
    var provider = RSA.Create();
    provider.FromXmlString(storedXml);

    var key = new RsaSecurityKey(provider);

    var tokenHandler = new JwtSecurityTokenHandler();

    var identity = new ClaimsIdentity(new[]
    {
        new Claim(JwtRegisteredClaimNames.Sub, user.FindFirst(ClaimTypes.NameIdentifier).Value),
        new Claim(ClaimTypes.Name, user.Identity.Name),
    });

    foreach (var role in user.Claims.Where(c => c.Type == ClaimTypes.Role))
        identity.AddClaim(new Claim(ClaimTypes.Role, role.Value));

    var tokenDescriptor = new SecurityTokenDescriptor
    {
        Subject = identity,
        Expires = DateTime.UtcNow.AddDays(7),
        Issuer = externalUrl,
        Audience = externalUrl,
        SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.RsaSha256Signature)
    };

    var token = tokenHandler.CreateToken(tokenDescriptor);
    var jwt = tokenHandler.WriteToken(token);
        
    

Finally the JWT is stored in a cookie:

        
    Response.Cookies.Append(JwtAuthenticationHandler.CookieName, jwt, new CookieOptions
    {
        Path = "/",
        SameSite = SameSiteMode.Lax,
    });
        
    

Validating a JWT

A JWT is signed using the RSA key. The public portion (only) of the key can safely be exposed, and then clients can use that public key to validate that the JWT has not been altered. The example public key is exposed here: public key

On client modules, the JwtAuthenticationHandler is added to the services:

        
    return services
        .AddAuthentication(JwtAuthenticationHandler.SchemeName)
        .AddScheme<AuthenticationSchemeOptions, JwtAuthenticationHandler>(JwtAuthenticationHandler.SchemeName, o => { });
        
    

When a request comes in, the JWT is validated to ensure the user is logged in:

        
    var publicKey = await DownloadPublicKeyAsync(externalUrl);
    var tokenHandler = new JwtSecurityTokenHandler();

    var principal = tokenHandler.ValidateToken(jwt, new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidateAudience = true,
        ValidIssuer = externalUrl,
        ValidAudience = externalUrl,
        IssuerSigningKey = publicKey
    }, out _);

    var ticket = new AuthenticationTicket(principal, SchemeName);
    return AuthenticateResult.Success(ticket);
        
    

Notes

It is important to remember that a JWT is not encrypted. It is cryptographically signed (so it cannot be forged), but the contents are always readable by any client that obtains it. Here is an example token from the application: JWT

The JWT was given an (arbitrary) expiration of 7 days. The cookie, however, was not given an expiry, so it is a 'session' cookie (i.e., it will be discarded when the browser is closed). The desired behaviour will be application specific: you may wish your cookies to be retained for a period, you may wish your JWT to be 'refreshed' after a short period (e.g., every 1 hour). For this example, if you leave your browser open for the 7 days, then the JWT will no longer be valid, and you will be redirected to the login page for your next request.

Some of the above code is pseudo code, but the complete working example is here: https://jwtauth.rgbco.uk/, and the source code is here: https://github.com/.../JwtAuth