top of page

Concurrent refresh tokens redemption without locks, in ASP.NET core

Writer's picture: Alejandro GaioAlejandro Gaio

Updated: Mar 21, 2024


Token exchange



Before we start


This post assumes you're familiar with concepts like json web tokens (JWTs), refresh tokens, authentication and, at some extent, tasks and concurrency management.


If that's not the case, I recommend you to look for those topics somewhere in the web, or just keep reading and when in doubt, go for/google it!



Background


I've been working for quite some time now on a Human Resources web platform.


The frontend is built with Razor Pages, and the backend consists of a .net core api and connected storage services: SQLServer and the file system for local development and SQL Azure and Blob storage accounts when deployed to Azure.


One of the first decisions I had to make was what kind of authentication should both, the web app and the api, use.


A quick shot would have been to chose json web tokens for both, so some code and logic could be shared or reused, but considering one is a classic web application, I decided that it should stick to cookies and leave the JWTs for the api.


The web app uses cookies as the authentication mechanism because they're easy to implement: browsers handle them transparently, they provide a convenient way to store the authentication ticket (which is not the cookie itself, this is just a wrapper), and it's attached automatically to every request the browser makes to the BFF (backend-for-frontend).


Talking about the BFF, it was built up using Razor Pages, which is a project type that works with server-side rendered pages, and consists mainly of html (.cshtml) files and a code-behind (.cs) files, like the old day's ASP.NET web forms, but much, much better.


The pages are the view part of an MVC (some says MVVM) model, and the code-behind file is the model and controller packed together. These page controllers are responsible of channeling communication with the backend api by using a proxy pattern, but more on this later.


A diagram of the components and their communication paths would something like this:

JWTs and refresh tokens

JWTs are a self-contained authorization and information exchange mechanism.


They are base64url-encoded strings composed of 3 parts:

  1. a header

  2. a payload

  3. a signature


The header tells which signing algorithm the JWT uses, like HS256, ES256 and so on.


The signature is used to verify that the token has not been tampered with.


The payload usually contains user claims (who the user is), expiration infotmation, etc.


Whoever has access to a JWT is treated as the authorized user, as long as the token hasn't yet expired.


That's why JWTs are so powerful: they don't depend upon a protocol, device type, platform, etc. They're just strings! They don't need a session in a server to keep user authentication properties because they're self-contained, and they have all the needed information (in form of claims) to access the authorized resources.


But with power comes responsibility.


JWTs are supposed to be short-lived, for the fact mentioned previously, that whoever has access to them is treated as the authorized user.


If a malicious user obtains access to a JWT, he will have a time window in order to exploit it until it expires. That's why they're usually issued with a lifetime of just a few minutes.


But being short-lived means that, upon expiration, a new JWT has to be obtained from the authorization endpoint. This is a problem for fronend apps, you cannot ask for user credentials in order to authenticate the user again every few minutes... can you?


This is where refresh tokens (RT) come in: they're like a lightweight ticket, that you exchange for a new JWT every time the latter has expired. As long as you have the ticket, you don't have to authenticate with credentials again.


Refresh tokens are returned along with JWTs as a result of an initial authentication process and, on the contrary to JWTs, they are stored by the api in a database, which is needed in order to validate (or invalidate) them.


JWTs have a specific structure detailed by RFC 7519, while RTs can adopt any form that fits the solution. In my case, I just used unique identifiers (guids), as they're practical.


When a request is received by the refresh token endpoint, the api looks for the token value it in the DB, where it was stored along with the user id it was issued to. With this information the api can build a new JWT.


The redeemed refresh token is then invalidated (i.e. deleted from the DB), and the new token pair (JWT + RT) are returned to the caller as an authentication payload.


Wait.. if a malicious user could access a JWT, what would happen if he has access to a refresh token (which has a longer expiration time)?


Well, in that case, you should have an invalidation mechanism in place in the api side, like deleting all refresh tokens from the DB, or a certain compromised group of them. This will cause the api to return an Unathorized response when a forged redemption attempt happens.


On the other hand, JWTs cannot be invalidated (as they're not stored anywhere in the token service) unless the key that signs them is changed. This of course, could cause a lot of issues in large systems (i.e. global rejections), and it's something you don't want to do.



How JWTs are used in the web app


The web application does not authenticate users by itself, but it sends user credentials to the api auth endpoint, which returns the payload with the jwt and a refresh token.


In the client side, this token pair is stored (wrapped) inside the cookie generated by the authentication middleware, using a class for that purpose called AuthenticationProperties:

// puts the JWT token in an authentication cookie. The cookie is encrypted using data protection api
var ap = new AuthenticationProperties() { IsPersistent = true };
ap.StoreTokens(new[] {
   new AuthenticationToken() { 
      Name = "jwt", 
      Value = authResult.token
      },
   new AuthenticationToken() {
      Name = "refresh_token", 
      Value = authResult.refreshToken.ToString() + "|" 
         + authResult.refreshTokenValidUntil.ToString("o", 
         System.Globalization.CultureInfo.InvariantCulture) }
});

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

So in the end, the browser receives the authentication cookie which in turn contains our tokens inside. The cookie, by the way, is encrypted and has the http only attribute, so it cannot be accessed by javascript.


When a page in the browser needs something from the backend api, it sends a request to the BFF (cookie included automatically), using the standard Fetch javascript api. The BFF unwraps the tokens from inside the cookie, and builds a request using bearer authentication against the backend api.



Implementing refresh tokens


The first release of the platform only issued JWTs (no refresh tokens), with a quite long expiration timeout, which of course is strongly discouraged for what I explained earlier.


The reason for this was that I had to build a minimum viable product (MVP) fast, and didn't have time to implement all the nitty gritty details of the app-api communication and refresh token redemption mechanism.


As the platform evolved (far beyond from a MVP), it was imperious to patch those loose security points, and the time had come to implement JWT refresh tokens.


Changes had to be applied in both the api and the web app sides.

  • On the api side, JWTs now have a very much shorter lifetime, and the authentication payload returned by the api now includes a refresh token, with a long time span, for the consumers to redeem for a new JWTs as needed.

  • On the web app side, the authentication cookie now stores the JWT and RT tokens together (as shown above). This wasn't a problem in itself... the only consequense was just a somewhat larger cookie, but within the spec limits.


I had to adapt the "unwrap JWT from cookie" procedure, detecting before every outgoing api call if the JWT has expired, and in that case redeem the refresh token for a new JWT. With the new JWT obtained, the flow continued as normal, like if the original JWT hadn't ever expired.



The concurrency problem


Relatively simple web applications (i.e. the ones that only render pages and use stardard get or post requests) may check if a JWT has expired, and in that case, redeem the refresh token for a new JWT. End of story.


But this web application evolved into a more complex frontend, that used concurrent fetch requests to refresh different parts of the screen in parallel: for instance, side menu badges and the user news feed, among other things.


More often than not, one of those parallel refreshes failed, and it didn't take me long to pinpoint the culprit: the same refresh token was being redeemed more than once, meaning the second, third and preceeding requests were trying to use an invalidated token. Boom!


A solution to this problem had to be implemented, and thread locking was clearly not the answer.


While digging around the web for similar problems, I found an interesting post of Bryan Helms that served me as an initial kick-off idea of what I had to do.



The analyzed solution

Before I explain the solution, I'd like you to picture the following 2 situations, the happy path and the sad path:


Happy path:

  1. A user logs in. A token pair is received from the api. JWT expires in 5 minutes.

  2. The home page is shown to the user, and it starts 2 ajax calls to the BFF to request the sidebar badges and the user news feed.

  3. The BFF receives both requests almost at the same time. Both requests come with the authentication cookie attached.

  4. On the BFF side:

    1. Thread 1 (badges): The BFF unwraps the JWT inside the cookie. It's ok (has not expired).

    2. Thread 2 (feed): The BFF unwraps the JWT inside the cookie. It's ok (has not expired).

    3. Thread 1: The BFF creates a request to the backend API using bearer authentication, sets JWT in the request header and sends it.

    4. Thread 2: The BFF creates a request to the backend API using bearer authentication, sets JWT in the request header and sends it.

    5. Thread 1: The BFF receives an OK response from the API and returns the results to the page.

    6. Thread 2: The BFF receives an OK response from the API and returns the results to the page.


Sad path:

  1. A user logs in. A token pair is received from the api. JWT expires in 5 minutes.

  2. The home page is shown to the user, and it starts 2 ajax calls to the BFF to request the sidebar badges and the user news feed.

  3. Until this point, everything happens exactly the same as the happy path scenario described above.

  4. The user walks away to get a coffee. He comes back after 10 minutes.

  5. He receives a notification from the platform: a new post was published and is now available in his user feed.

  6. This triggers a UI refresh procedure to update the badges and the news feed.

  7. The BFF receives both requests almost at the same time. Both requests come with the authentication cookie attached.

  8. On the BFF side:

    1. Thread 1 (badges): The BFF unwraps the JWT inside the cookie. It's expired, so the redeem refresh token procedure runs. The BFF sends the refresh token to the /refresh api endpoint.

    2. Thread 2 (feed): The BFF unwraps the JWT inside the cookie. It's expired, so the redeem refresh token procedure runs. The BFF sends the refresh token to the /refresh api endpoint.

  9. On the api side:

    1. Thread 1: Checks the refresh token in the DB. It's valid. The api builds a new token pair and invalidates the received RT. The authentication payload is returned.

    2. Thread 2: Checks the refresh token in the DB. It's invalid, since it was already redeemed by thread 1. The api returns an Unauthorized response.

  10. On the BFF side again:

    1. Thread 1: The BFF receives the new authentication payload and creates a request to the api using the new JWT. It then fulfills the badges update part.

    2. Thread 2: The BFF receives the Unathorized response. It returns an error code to the page to show that the feed could not be loaded.

Now that you get the picture, let's talk about one of the first things that come to mind to developers when we have to deal with concurrency: locks.



Why locking is bad (in this context)


When working with asynchronous code, it's important not to block the running thread, which defeats the purpose of being async.


But when an resource must be protected from simultaneous access (in this case, the /refresh api endpoint), some other mechanism has to replace locking.


Not only is thread locking a bad solution in this context, because it prevents the scheduler from switching to other tasks, but it also imposses a serious performance penalty and thread hog risk in the server (BFF).


As a side note, if you're inside an async block (which most probably you are when working with Razor Pages), you may use lock, but you cannot await from inside it because of compiler restrictions.


So, if locking has no place in the solution to this problem, what else could we resort to?


The answer to this question is the implementation of two components that work together:

  • A concurrent dictionary that uses a special overload: one that accepts a factory method to create new values

  • A lazy-derived, generic class that is also awaitable, which will be the return value of the factory method mentioned above: AsyncLazy<T>

The concurrent dictionary


It's purpose is to keep track of the refresh tokens that need to be redeemed to the /refresh api endpoint.

  • It's a dictionary, because we want to store only one instance of a RT at a time (key), no matter how many frontend request with that same token are received.

  • It's concurrent because it handles in a light and graceful way adding and getting values in a multithreaded environment.

A JWT that must be used can be alive or expired. If it's still alive, the request is sent to the api and that's it.


On the other hand, if the JWT has expired, the refresh token is checked. If it has also expired, a whole authentication process is triggered: sign out (from cookies auth) followed by a redirect to the login page.


But if the RT is still usable, it enters in the redemption process that makes use of the concurrent dictionary.


During this process, multiple requests may want to redeem the same refresh token as it was the case of the badges and user news feed.


When two or more simultaneous requests come from the same source (recall: same user -> same cookie -> same token pair), the first request will add the RT token to the dictionary, and subsequent requests will get the value associated with that same token, because we use the convenient GetOrAdd method :


ConcurrentDictionary.GetOrAdd(string key, Func<TKey, TValue> valueFactory)

This overloaded method does not store a value directly in the dictionary, but instead it calls a factory method which is responsible for creating the value we want to add, bound to that key.



The AsyncLazy<T>


The factory method specified in GetOrAdd returns a new instance of AsyncLazy<AuthResultWrapper> by calling TokenFactory:

var tokenEntry = _tokens.GetOrAdd(token, key => new AsyncLazy<AuthResultWrapper>(() => TokenFactory(key)));

Important! To avoid confusion with the class names mentioned in this section, AuthResult is the bare api response we receive from the backend, and AuthResultWrapper is a local, lightweight wrapper around AuthResult used to facilitate dictionary entry purging and management of api operation outcomes. You should think of them as the same logical object.


AuthResultWrapper is the result we're expecting from the api call, but the AsyncLazy instance is initialized with a lambda expression that, in the end, will execute the api call only when its Value property is accessed. That's the lazy part!


The TokenFactory function does the following:

  • It creates an HttpClient

  • Posts a request to the /refresh api endpoint using the RT token

  • If successful, it wraps the authentication payload (AuthResult) containing the new token pair in an AuthResultWrapper.

The use of the lazy pattern allows us to defer the api request, so it doesn't happen as soon as the new item is added to the dictionary.



Awaiting AsyncLazy


Our AsyncLazy class implements the GetAwaiter method, thus making this class awaitable.


When awaiting an AsyncLazy object:

-> the GetAwaiter method is called,

-> which in turn accesses the base class' Value property,

-> which is a lambda expression that calls TokenFactory

-> which makes an api call and returns a result


To get a better understanding, take a look at the following code snippets:


AsyncLazy class:

public class AsyncLazy<T> : Lazy<Task<T>>
{
	public AsyncLazy(Func<T> valueFactory)
		: base(() => Task.Factory.StartNew(valueFactory)) { }

	public AsyncLazy(Func<Task<T>> taskFactory)
		: base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap()) { }

	// This allows awaiting the AsyncLazy, 
	// which in turn will read the .Value property, 
	// which in turn will run the task specified by 
	// the task/valueFactory parameter in the constructor
	public TaskAwaiter<T> GetAwaiter()
	{ return Value.GetAwaiter(); }
}


The implementation


The BFF makes use of the proxy pattern to group all api calls in a one place.


This proxy is then injected to every page or controller that needs to consume the backend api.


Every proxied api call in this class makes use of a private method called GetClient, which is responsible of unwrapping the tokens from the cookie, checking if the JWT can be used, and if not, evaluate if a RT redemption or a redirect to login must happen.


This GetClient method is what I mentioned earlier as "unwrap JWT from cookie" in the Implementing refresh tokens section.


When a refresh token redemption must be carried on, the GetClient code relies on a token store (IRefreshTokenStore), explained later.

The code of GetClient is the following:

private IRefreshTokenStore _refreshTokenStore; // injected via DI

private async Task<HttpClient> GetClient()
{
	var jwt = await _context.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, JwtAuthTicketFormat.TokenConstants.TokenName);
	if (jwt is null)
	{
		_logger?.LogWarning($"JWT token is null in {nameof(HRApiServiceWrapper)}.{nameof(GetClient)}");
		throw new HRSessionExpiredException();
	}

	SecurityToken token = default;
	try
	{
		token = JwtOnCookieHelper.GetSecurityToken(jwt, out var claimsPrincipal);
	}
	catch (Exception ex)
	{
		_logger?.LogError(ex, $"Error validating token @ {nameof(GetClient)}");
		throw new HRSessionExpiredException();
	}

	// check if JWT has expired
	if (token.ValidTo < DateTime.UtcNow)
	{
		_logger?.LogInformation($"JWT token is expired. About to redeem refresh token...");
		var refreshTokenComposite = await _context.GetTokenAsync(
			CookieAuthenticationDefaults.AuthenticationScheme, 	
			"refresh_token");
		if (string.IsNullOrEmpty(refreshTokenComposite))
		{
			_logger?.LogWarning($"Refresh token is null in {nameof(HRApiServiceWrapper)}.{nameof(GetClient)}");
			throw new HRSessionExpiredException();
		}

		// refresh token is stored in the cookie with the format 
		// [refresh token]|[refresh token expiration]
		// that's why we call it 'composite'
		if (!refreshTokenComposite.Contains("|"))
		{
			_logger?.LogError($"Refresh token format is invalid in {nameof(HRApiServiceWrapper)}.{nameof(GetClient)}");
			throw new HRSessionExpiredException();
		}

		var parts = refreshTokenComposite.Split('|');
		var refreshToken = parts[0];
		if (!DateTime.TryParse(parts[1], out DateTime refreshTokenExpiration))
		{
			_logger?.LogError($"Refresh token expiration date is invalid in {nameof(HRApiServiceWrapper)}.{nameof(GetClient)}");
			throw new HRSessionExpiredException();
		}

		if (refreshTokenExpiration < DateTime.UtcNow)
		{
			_logger?.LogInformation($"Refresh token {refreshToken} is also expired");
			throw new HRSessionExpiredException();
		}

		_logger?.LogInformation($"Expired JWT token about to be redeemed using refresh token {refreshToken}.");

		// if refresh token is valid, redeem it for a new token pairs
		// IRefreshTokenStore is a concurrent dictionary 
		// that stores refresh tokens.
		// It's used because when parallel request with the same 
		// refresh token are received,
		// only one of them should be used to redeem the 
		// refresh token, and the others
		// should wait the task and use the new JWT obtained
		var rtResult = await _refreshTokenStore.GetTokenAsync(refreshToken, true, _context);
		if (rtResult is null)
			throw new HRSessionExpiredException();

		jwt = rtResult.token;
	}

	// create an HttpClient using IHttpClientFactory
	var client = _clientFactory.CreateClient(Globals.HRAPI_HTTP_CLIENT);
	client.DefaultRequestHeaders.Add("Authorization", "Bearer " + jwt);
	client.DefaultRequestHeaders.Add("Accept-Language", Thread.CurrentThread.CurrentCulture.Name);
	return client;
}

Note: JwtOnCookieHelper code will be shown later as it's not relevant for this section.


I made this flowchart to show how this process works. My apologies for the layout, but I had to make it fit in the page. The blue area titled "RefreshTokenStore" is part of the IRefreshTokenStore implementation, which will be shown right after the diagram:



In order to handle refresh tokens inside the proxy class, an interface and an implementation were created, the latter being injected into the proxy through dependency injection.


The IRefreshTokenStore is added to the DI container with:

services.AddSingleton<IRefreshTokenStore, RefreshTokenStore>();

IRefreshTokenStore interface:

public interface IRefreshTokenStore
{
	Task<AuthResult> GetTokenAsync(string token, HttpContext context);
}

Warning! Please don't confuse IRefreshTokenStore.GetTokenAsync with HttpContext.GetTokenAsync which is an extension method from the Microsoft.AspNetCore.Authentication package.


RefreshTokenStore class:

public class RefreshTokenStore : IRefreshTokenStore
{
	private readonly IHttpClientFactory _clientFactory;
	private readonly ILogger<IRefreshTokenStore> _logger;

	private readonly ConcurrentDictionary<string, AsyncLazy<AuthResultWrapper>> _tokenDic = new();

	public RefreshTokenStore(IHttpClientFactory httpFactory, ILogger<IRefreshTokenStore> logger)
	{
		_clientFactory = httpFactory;
		_logger = logger;
	}

	public async Task<AuthResult> GetTokenAsync(string token, HttpContext context)
	{
		var entry = _tokenDic.GetOrAdd(token, key => new AsyncLazy<AuthResultWrapper>(() => TokenFactory(key)));

		// The dict stores Tasks, so we need to 
		// await to get the actual token
		try
		{
			// this await runs the TokenFactory
			// method, which starts the
			// token redemption process
			var wrapper = await entry; 
			
			AuthResult authResult = null;
			if (wrapper.IsExpired || wrapper.IsFailed)
			{
				if (wrapper.IsExpired)
				{
					_logger.LogWarning($"An operation with refresh token {token} may have taken too much time, so the JWT token expired.");
				}

				// If a token is expired or the request to 
				// renew failed, we should revoke it from our store.
				// Use TryRemove because a different caller may 
				// have gotten here before us,
				// and we don't care about throwing in this case.
				_tokenDic.TryRemove(token, out _);

				return null; // null signals redirect to login
			}

			authResult = wrapper.AuthResult;

			// this replaces the cookie in the client browser
			// with a new one, containing the new JWT token
			await JwtOnCookieHelper.SignInUsingJwt(authResult, context);
			_logger.LogInformation($"JWT Token obtained from refresh {authResult.refreshToken} (previous {token}) used to sign in on context.");

			return authResult;
		}
		catch (Exception ex)
		{
			_logger.LogError(ex, $"Error with token {token} @ {nameof(GetTokenAsync)}.");
			return null;
		}
		finally
		{
			await PurgeOldCacheEntries();
		}
	}

	private async Task<AuthResultWrapper> TokenFactory(string refreshToken)
	{
		try
		{
			var client = _clientFactory.CreateClient(Globals.HRAPI_HTTP_CLIENT);
			client.DefaultRequestHeaders.Add("Accept-Language", Thread.CurrentThread.CurrentCulture.Name);

			string m = default;
			var response = await client.GetAsync($"/api/auth/refreshtoken?rt={refreshToken}", content);
			if (!response.IsSuccessStatusCode)
			{
				if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
				{
					m = $"Refresh token {refreshToken} rejected @ {nameof(TokenFactory)}.";
					_logger.LogWarning(m);
					return new AuthResultWrapper(new Exception(m));
				}
				else {
					m = $"Error refreshing token {refreshToken} @ {nameof(TokenFactory)}. Status={response.StatusCode}. No more retries available.";
					_logger.LogError(m);
					return new AuthResultWrapper(new Exception(m));
				}
			}

			var messageContents = await response.Content.ReadAsStringAsync();
			var token = JsonSerializer.Deserialize<AuthResult>(messageContents);

			_logger.LogInformation($"Token {refreshToken} redeemed successfully. New refresh token is {token.refreshToken}");

			return new AuthResultWrapper(token);
		}
		catch (Exception ex)
		{
			_logger.LogError(ex, $"Unexpected error refreshing token @ {nameof(TokenFactory)}.");
			return new AuthResultWrapper(ex);
		}
	}

	private async Task PurgeOldCacheEntries()
	{
		foreach (var kvp in _tokenDic)
		{
			var cacheEntry = kvp.Value;
			var t = cacheEntry.Value;
			if (t.IsCompleted)
			{
				var wrapper = await t;
				if ((DateTime.UtcNow - wrapper.TimeStamp).TotalMinutes > 5)
					_tokenDic.TryRemove(kvp.Key, out _);
			}
		}
	}
}

The first line inside the try..catch of GetTokenAsync is the await statement:


var wrapper = await entry;

So, suppose two concurrent request with the same refresh token arrive to the BFF, this is what happens:

  1. Request 1: Stores a new entry into the concurrent dictionary, using the token as key and an uninitialized AsyncLazy instance as the value.

  2. Request 2: Gets the same AsyncLazy<AuthResultWrapper> instance that was already stored by request 1, because we're using GetOrAdd from the dictionary and the key is the same.

  3. Request 1: the await keyword causes the AsyncLazy to read its underlaying value (which is a Task) through the GetAwaiter. This in turn executes the TokenFactory method, initiating the RT redemption process.

  4. Request 2: since the AsyncLazy object obtained is the same a the one stored by request 1, the await keyword causes request 2 to wait for that same task to complete. This would be true even if there were more than 2 parallel requests using the same RT.

  5. Request 1: when the api call returns, the code continues after the await and the new token pair is returned.

  6. Request 2: code continues after the await since the task is now completed, releasing code execution, and returning the same token pair as in step 5.


The other code snippets that complete the picture are shown below: the AuthResult and AuthResultWrapper classe, and the JwtOnCookieHelper class:


AuthResult and AuthResultWrapper classes:

public class AuthResult
{
	/// <summary>
	/// The JWT token
	/// </summary>
	public string token { get; set; }

	/// <summary>
	/// The expiration of the JWT token
	/// </summary>
	public DateTime expires { get; set; }

	/// <summary>
	/// The refresh token
	/// </summary>
	public Guid refreshToken { get; set; }

	/// <summary>
	/// The expiration of the refresh token
	/// </summary>
	public DateTime refreshTokenValidUntil { get; set; }

	public bool IsTokenExpired => expires < DateTime.UtcNow;
	public bool IsRefreshTokenExpired => refreshTokenValidUntil < DateTime.UtcNow;
}
	
public class AuthResultWrapper
{
	public AuthResultWrapper(AuthResult a) {
		if (a is null)
			throw new NullReferenceException($"a is null in {nameof(AuthResultWrapper)} ctor.");
		AuthResult = a;
	}

	public AuthResultWrapper(Exception ex) {
		FailedException = ex;
	}

	public AuthResult AuthResult { get; private set; }
	public DateTime TimeStamp { get; private set; } = DateTime.UtcNow;
	public bool IsExpired => AuthResult?.IsTokenExpired ?? false;
	public bool IsFailed => FailedException != null;
	public Exception FailedException { get; private set; }
}

JwtOnCookieHelper class:

public class JwtOnCookieHelper
{
	public static string Issuer { get; set; }
	public static string Audience { get; set; }

	public static TokenValidationParameters TokenValidationParameters
	{
		get
		{
			return new TokenValidationParameters()
			{
				ValidateIssuerSigningKey = false,
				ValidateAudience = true,
				ValidAudience = Audience,
				ValidateIssuer = true,
				ValidIssuer = Issuer,
				ValidateLifetime = false, // changed to false, to allow validation of expired tokens. Lifetime will be validated in code.
				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);
				}
			};
		}
	}

	public static SecurityToken GetSecurityToken(string jwtString, out ClaimsPrincipal principal)
	{
		var th = new JwtSecurityTokenHandler();
		th.MapInboundClaims = false;    // this is important to avoid JwtSecurityTokenHandler mapping claims like 'sub' to 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'
		var claimsPrincipal = th.ValidateToken(jwtString, TokenValidationParameters, out SecurityToken vt);
		principal = claimsPrincipal;
		return vt;
	}

	public static async Task SignInUsingJwt(AuthResult authResult, HttpContext context) {
		// this part validates the token received from the api
		var validatedToken = GetSecurityToken(authResult.token, out ClaimsPrincipal claimsPrincipal);

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

		// puts the JWT token in an authentication cookie. 
		// The cookie is encrypted using data protection api
		var ap = new AuthenticationProperties() { 
			IsPersistent = true 
		};
		ap.StoreTokens(new[] {
			new AuthenticationToken() { 
				Name = "jwt", 
				Value = authResult.token 
			},
			new AuthenticationToken() {
				Name = "refresh_token", 
				Value = authResult.refreshToken.ToString() + "|" + authResult.refreshTokenValidUntil.ToString("o", System.Globalization.CultureInfo.InvariantCulture) }
			});
		
		await context.SignInAsync(
			CookieAuthenticationDefaults.AuthenticationScheme,
			claimsPrincipal,
			ap);
	}
}


Wrapping up


Thoughout this post I described how to avoid using locks when dealing with asynchronous code that needs to control access to a resource from multiple threads.


The resource we're controlling access to is an external backend api endpoint, used to redeem refresh tokens.


Note that I said 'to control' and not 'to enforce exclusive' access, because we only wanted to control simultaneous attempts to redeem the same refresh token, not every refresh token..


This was accomplished by using a combination of a thread-safe concurrent dictionary that uses the refresh tokens as keys, and a lazy-derived class that wraps the protected resource's access logic as values.


The hardest part is (or maybe for me it was) building a mental map of all those references that come up when dealing with tasks, lambda expressions, and so on, when they're chained together or wrapped one inside another.


But the important thing to notice is that the code can handle multiple requests from different clients without locking.


And when the requests are from the same authenticated client (who uses the same refresh token), the combination of a concurrent dictionary and a lazily initialized resource make it possible to group those requests in a single task and make them all wait for the result, all in an async-friendly, non-locking way.


Please leave me a comment if you find this post useful, confusing, found bugs or just wanted to say thanks! C-U


26 views0 comments

Recent Posts

See All

Kommentare


©2022 by Alejandro Gaio.

bottom of page