Search code examples
asp.net-coreredisazure-ad-msalmicrosoft-identity-platform

Token caching in ASP.NET Core 8 using MSAL, Redis and Keycloak


In my ASP.NET Core 8 application, I am trying to cache access tokens using ConfidentialClientApplicationBuilder from Microsoft.Identity.Client (v4.59). As cache, I am using Redis and as IdentityProvider I am using Keyloak.

Redis setup:

services.AddDistributedTokenCaches();
services.AddStackExchangeRedisCache(options =>
{
  options.Configuration = redisSettings.ConnectionString;
  options.InstanceName = redisSettings.InstanceName;
});

Getting tokens:

var app = ConfidentialClientApplicationBuilder
  .Create("myClientId")
  .WithClientSecret("myClientSecret")
  .WithExperimentalFeatures()
  .WithGenericAuthority("https://mykeycloakinstance/auth/realms/master/")
  .Build();

// _cacheProvider is injected IMsalTokenCacheProvider (redis)
_cacheProvider.Initialize(app.UserTokenCache);
_cacheProvider.Initialize(app.AppTokenCache);

var token = await app.AcquireTokenForClient(_scopes).ExecuteAsync();
return token.AccessToken;

The problem is that the first call to get the token returns a Microsoft.Identity.Client.MsalClientException (but does create a Redis entry):

ErrorCode: combined_user_app_cache_not_supported
Source: Microsoft.Identity.Client
Message: Using a combined flat storage, like a file, to store both app and user tokens is not supported. Use a partitioned token cache (for ex. distributed cache like Redis) or separate files for app and user token caches. See https://aka.ms/msal-net-token-cache-serialization .
StackTrace:
   at Microsoft.Identity.Client.PlatformsCommon.Shared.InMemoryPartitionedAppTokenCacheAccessor.SaveIdToken(MsalIdTokenCacheItem item)
   at Microsoft.Identity.Client.TokenCache.<Microsoft-Identity-Client-ITokenCacheInternal-SaveTokenResponseAsync>d__55.MoveNext()
   at Microsoft.Identity.Client.TokenCache.<Microsoft-Identity-Client-ITokenCacheInternal-SaveTokenResponseAsync>d__55.MoveNext()
   at Microsoft.Identity.Client.Cache.CacheSessionManager.<SaveTokenResponseAsync>d__10.MoveNext()
   at Microsoft.Identity.Client.Internal.Requests.RequestBase.<CacheTokenResponseAndCreateAuthenticationResultAsync>d__22.MoveNext()
   at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.<GetAccessTokenAsync>d__4.MoveNext()
   at Microsoft.Identity.Client.Internal.Requests.ClientCredentialRequest.<ExecuteAsync>d__3.MoveNext()
   at Microsoft.Identity.Client.Internal.Requests.RequestBase.<RunAsync>d__12.MoveNext()
   at Microsoft.Identity.Client.ApiConfig.Executors.ConfidentialClientExecutor.<ExecuteAsync>d__3.MoveNext()

The second call works fine and returns the cached access token. The same issue happens when the Redis entry expires (the first call fails with the same error and the second call succeeds).

Any idea how to fix the issue or is there some other library that could be used for the same purpose (I don't like having to use WithExperimentalFeatures for my OIDC Identity Provider)?


Solution

  • Found the cause and a fix for the issue.

    Original request contained openid scope which results in Keycloak returning both an IdToken and an AccessToken in the response.

    AccessToken from the response was successfully saved in Redis by the Microsoft.Identity.Client library and then an exception with a somewhat confusing message was thrown by the library (when trying to save the IdToken).

    Subsequent calls before the cache entry expired did not try to get the Keycloak response, but used the cache instead (which did contain a correctly cached AccessToken).

    After openid scope was removed from the request, response did not contain an IdToken and the first call to AcquireTokenForClient passed.