5

Random Blurbs and Things of this Nature

 3 years ago
source link: https://jfarrell.net/2021/03/09/manual-jwt-validation-in-net-core/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Manual JWT Validation in .NET Core

Recently, I have been working with Jeff Fritz over at https://www.twitch.tv/csharpfritz as part of his effort to build a TikTok like site for Twitch, uniquely called KlipTok (https://www.kliptok.com). Mainly my efforts have been on shoring up the backend code in the BackOffice using Azure Functions.

This was one of my first major exposures with the Twitch API. Its fine overall but, it oddly does not use JWT tokens to communication states back and forth, rather an issues string is required for authenticated requests. I wanted to try a different approach to handling token auth and refresh so, I devised the following POC: https://github.com/jfarrell-examples/TwitchTokenPoc.

One of the aspects of the Twitch API is that tokens can expire and calls should be ready to refresh an access token which enters this state. The trouble is, these are two tokens and I didnt want the clients required to send both tokens, nor did I want the client to have to resubmit a request. I decided, I would create my own token and store within it, as claims, the access token and refresh token.

Taking this approach would allow the POC to, in effect, make it seem like Twitch is issues JWT tokens while still allowing the backend to perform the refresh. I decided, for additional security, I would encrypt the token claims in my JWT using Azure Key Vault Keys.

Part 1: Creating the Token

This approach hinges on what I refer to as token interception. As part of any OAuth/OIDC flow, there is a callback after the third party site (Twitch in this case) has completed the login. Tokens are sent to this callback for the sole purpose of allowing the caller to store them.

In order to achieve this, I created a method which a client would call at the very start. This contacts Twitch and reissues the active tokens, if they exist, or requests the user to login in again:

public IActionResult Get() { var redirectUri = WebUtility.UrlEncode("https://localhost:5001/home/callback"); var urlString = @$"https://id.twitch.tv/oauth2/authorize?client_id={_configuration["TwitchClientId"]}" + $"&redirect_uri={redirectUri}" + "&response_type=code" + "&scope=openid";

return Redirect(urlString); }

The key here is the redirectUri which redirects the provided response code back to the application. Here we can create the token and send it to the client. You can find this method in the provided GitHub repository, HomeController.

You can find MANY examples of creating a JWT Token on the internet, I will use this one for reference: https://www.c-sharpcorner.com/article/asp-net-web-api-2-creating-and-validating-jwt-json-web-token/

Here is my code which creates the token string with the access token and refresh token as claims:

public async Task<string> CreateJwtTokenString(string accessToken, string refreshToken) { var jwtSigningKey = await _keyVaultService.GetJwtSigningKey(); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSigningKey)); var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature); var secToken = new JwtSecurityToken( issuer: _configuration["Issuer"], audience: _configuration["Audience"], claims: new List<Claim> { new Claim("accessToken", await _cryptoService.Encrypt(accessToken)), new Claim("refreshToken", await _cryptoService.Encrypt(refreshToken)) }, notBefore: null, expires: DateTime.Now.AddDays(1), signingCredentials); return new JwtSecurityTokenHandler().WriteToken(secToken);

The actual signing key is stored as a secret in Azure Key Vault with access controlled using ClientSecretCredentials, those values are stored in environment variables and not located in source code. You can find more information on this approach here: https://jfarrell.net/2020/07/14/controlling-azure-key-vault-access/ The one critical point I will make is ClientSecretCredential is only appropriate for local development – when deploying into Azure be sure code is using a Managed Identity driven approach.

I defined a simple method which grabs the Encryption key from Azure Key Vault and encrypts (or decrypts the data).

// getting the key private KeyClient KeyClient => new KeyClient( vaultUri: new Uri(_configuration["KeyVaultUri"]), credential: _getCredentialService.GetKeyVaultCredentials());

public async Task<KeyVaultKey> GetEncryptionKey() { var keyResponse = await KeyClient.GetKeyAsync("encryption-key"); return keyResponse.Value; } // usage public async Task<string> Encrypt(string rawValue) { var encryptionKey = await _keyVaultService.GetEncryptionKey(); var cryptoClient = new CryptographyClient(encryptionKey.Id, _getCredentialService.GetKeyVaultCredentials()); var byteData = Encoding.Unicode.GetBytes(rawValue); var encryptResult = await cryptoClient.EncryptAsync(EncryptionAlgorithm.RsaOaep, byteData); return Convert.ToBase64String(encryptResult.Ciphertext); }

The beauty of using Azure Key Vault is NO ONE but Azure is aware of the key. Using this, even if our JWT token is somehow leaked, the data within is not easy to decipher.

Once generated, this token can be passed back to the client either as data or in some header, allowing the client to store it. We can then use the built-in validation to require the token with each call.

Part 2: Validating the Token

Traditionally, tokens are signed by an authority and the underlying system will contact that authority to validate the token. However, in our case, we have no such authority so, we will want to MANUALLY validate the token, mainly its signature.

It turns out this is rather tricky to perform in ASP .NET Core due to the way the validation middleware is implemented. The best way I found to get it work and be clean is to adjust the way you register certain dependencies in ConfigureServices, as such:

var keyVaultService = new KeyVaultService(new GetCredentialService(Configuration), Configuration); var tokenSecurityValidator = new JwtSecurityTokenValidator(Configuration, keyVaultService);

services.AddTransient<CryptoService>() .AddTransient<JwtTokenService>() .AddSingleton<TwitchAuthService>() .AddSingleton(p => keyVaultService) .AddTransient(p => tokenSecurityValidator) .AddSingleton<GetCredentialService>() .AddTransient<TwitchApiService>() .AddTransient<GetTokensFromHttpRequestService>() .AddTransient<ProcessApiResultFilter>(); // add auth middleware services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.RequireHttpsMetadata = false; options.SecurityTokenValidators.Add(tokenSecurityValidator); });

You can see the keyVaultService and tokenSecurityValidator are defined as concrete dependencies and we use the provider override syntax for AddSingleton to pass the instance directly. This is done so we can pass the direct instance of tokenSecurityValidator to our the options for validating our Bearer token.

This class calls on its dependencies and validates the signature of the token and ensures it matches with our expectations: https://github.com/jfarrell-examples/TwitchTokenPoc/blob/master/JwtSecurityTokenValidator.cs

The result of adding this (and the appropriate Use methods in the Configure method) is we can fully leverage [Authorize] on our actions and controllers. Users who pass no token or a token that we cannot validate will receive a 401 Unauthorized.

Part 3: Performing the Refresh

First step with any call is the ability to GET the token for the request so it can be used. There are MANY ways to do this. As I wanted to keep this simple I elected to use the IHttpContextAccessor. This is a special dependency you can have ASP .NET Core inject that lets you access the HttpContext anywhere in the call chain. I wrapped this in a service:
https://github.com/jfarrell-examples/TwitchTokenPoc/blob/master/Services/GetTokensFromHttpRequestService.cs

This class very simply yanks the token from the incoming request and return the specific claim that represents the token. It also calls the decryption method so the fetched token is ready for immediate use.

This is by no means a perfect approach, in fact were I to see this in Production code I would comment that its a violation of the separation of concerns since a web concerns is being accessed in the services layer. More ideally, you would want to use middleware or similar to hydrate a scoped dependency which can be injected into your layers.

The TwitchApiService (https://github.com/jfarrell-examples/TwitchTokenPoc/blob/master/Services/TwitchApiService.cs) houses the logic to perform the request for user data from Twitch that I chose to showcase the refresh functionality.

This code is crucial for the functionality:

client.DefaultRequestHeaders.Add("Client-Id", _configuration["TwitchClientId"]); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", await _getTokensFromHttpRequestService.GetAccessToken());

var result = new ApiResult<TwitchUser>(); var response = await client.GetAsync($"helix/users?login={loginName}"); if (response.StatusCode == HttpStatusCode.Unauthorized) { // refresh tokens var (accessToken, refreshToken) = await _authService.RefreshTokens(await _getTokensFromHttpRequestService.GetRefreshToken()); result.TokensChanged = true; result.NewAccessToken = accessToken; result.NewRefreshToken = refreshToken; // re-execute the request with the new access token client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); response = await client.GetAsync($"helix/users?login={loginName}"); }

if (response.IsSuccessStatusCode == false) throw new Exception($"GetUser request failed with status code {response.StatusCode} and reason: '{response.ReasonPhrase}'");

var responseContent = await response.Content.ReadAsStringAsync();

I wrote this in a very heavy fashion, it simple makes the call, check if it failed with a 401 Unauthorized and, if so, refreshes the token using the TwitchAuthService () and then makes the same call again.

The result is a return to the caller with the appropriate data (or an error if the request still failed).

Part 4: Notify of new Token

Something you may have noticed in the previous code, the use of a generic ApiResult<T>. This is necessary because JWT tokens are designed to be immutable. This means they cannot be changed once created, its this aspect which makes them secure. However, in this case, we are creating a token with data that will change (on a refresh) and thus necessitate a regeneration of the token.

The purpose of this ApiResult<T> class it to hold NOT JUST the result but to tell us if the token needs to change. If it does change, that new version must be passed to the client so it can be saved. This may seem like a drawback to the approach but, in actuality this is a typical part of any application interacting with an OAuth flow where token refresh is being used.

However, what we DO NOT want to do is require logic in every action to check the result, rebuild the token, and pass it to the caller. Instead, we want to intercept the return result and, in a central spot, strip away the extra data and ensure our new token, if appropriate, is in the response headers.

To that end I created the following ActionFilter:

public class ProcessApiResultFilter : IActionFilter { private readonly JwtTokenService _jwtTokenService;

public ProcessApiResultFilter(JwtTokenService jwtTokenService) { _jwtTokenService = jwtTokenService; } public void OnActionExecuting(ActionExecutingContext context) { // no action }

public void OnActionExecuted(ActionExecutedContext context) { if ((context.Result as OkObjectResult)?.Value is ApiResult result) { if (result.TokensChanged) { var newTokenString = _jwtTokenService.CreateJwtTokenString( result.NewAccessToken, result.NewRefreshToken).Result; context.HttpContext.Response.Headers.Add("X-NewToken", newTokenString); } context.Result = new ObjectResult(result.Result); } } }

Our ApiResult<T> inherits from ApiResult which gives it the non-generic read only Result property, which is used in the code sample above. The ApiResult<T> includes a setter whose accepted type is T. This allows the application to interact with it in a type-safe way.

Above you can see the Result being sent to the user is altered so its the inner result. Meanwhile, if the token changes we regenerate that token using our JwtTokenService and its stored in the X-NewToken header in the response. Client can now check for this header when receiving the response and update their stores as needed.

One final thing, I am using Dependency Injection in the filter. To achieve this you must wrap its usage in the ServiceFilterAttribute. Example here: https://github.com/jfarrell-examples/TwitchTokenPoc/blob/master/Startup.cs#L27

And that is it. Let’s walk through the example again.

Understanding what happens

A given client will make its initial page the response to /Login which will return the Twitch Login screen OR, if a token is already present, the callback will be called instantly. This callback will generate a token and send it down to the caller (right now its printed to the screen), generally this would be a page in your client app that will store the token and show the initial page.

When the client makes a request, they MUST pass the custom JWT Token given to them, the application will be checking for it as an Authorization Bearer token – failure to pass it will result in a 401 Unauthorized being sent back.

The application, after validating the token, will proceed with its usual call to the Twitch API. Part of this will access whatever the Access Token was passed. If Twitch responds with a 401 Unauthorized, the code will extract the refresh token from the JWT Token and refresh the access token. Upon successfully doing this, the call to Twitch will be executed again.

The result is sent back to the caller in a wrapper, ApiResult<T> which, along with carrying the call result, also contains information on whether the token changed. The caller will simply return this result as it would any normal Action call.

We use a special ActionFilter to intercept the response, and rewrite it so the caller returns the expected result in the response body. If the token did change, the new token is written into the response behind the X-NewToken header.

Throughout the process, we never reveal the tokens and all of the values involved in signing, encryption, and decryption are stored in Azure Key Vault outside of our application. For local dev, we are using an App Registration to govern access to the Key Vault, if we were deployed in Azure we would want to associate our Azure service to a managed identity.

Conclusion

Hopefully, this example has been instructive and helpful. I know I learned quite a bit going through this process. So, if it helps you, drop me a comment and let me know. If something does not make sense feel free to also drop me a comment. Cheers.

Advertisements
Report this ad

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK