IdentityModel / IdentityModel.AspNetCore

ASP.NET Core helper library for claims-based identity, OAuth 2.0 and OpenID Connect.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

`CacheLifetimeBuffer` past date exception

coolhome opened this issue · comments

I use a specific web service who generates a JWT access token but does something odd. They are returning the same JWT for each subsequent request until its expired.

This leads to a cache expiration date cannot be in the past exception. Setting CachelifetimeBuffer to 0 resolves this however loses the benefit of refreshing early for other oauth resources I consume.

System.InvalidOperationException: The absolute expiration value must be in the future.

I use a specific web service who generates a JWT access token but does something odd. They are returning the same JWT for each subsequent request until its expired.

A little confused by this. This is how JWTs in the OAuth spec usually work, no? You get a token from IS4, and you re-use it again and again until it expires. You don't get a new token for every single request. What if you had 100 requests a second, would you want to slam IS4 and ask for 100 tokens in addition to the 100 requests to have to make? You store or cache the access token and re-use it until it expires, and then you renew/refresh it shortly before expiration and you do the same process with the new token.

Im not sure about "The absolute expiration value must be in the future." it sounds to me like they are sending you an expired token.

@VictorioBerra The WebService is Adobe Marketo REST API. I am using the Oauth Token route but keeps returning the last JWT created no matter way. Until the last previous token is truly expired it does not allow me to generate a new access token on demand. 🤷

https://developers.marketo.com/rest-api/authentication/

The problem is here:

public async Task SetAsync(string clientName, string accessToken, int expiresIn)
{
if (clientName is null) throw new ArgumentNullException(nameof(clientName));
var expiration = DateTimeOffset.UtcNow.AddSeconds(expiresIn);
var expirationEpoch = expiration.ToUnixTimeSeconds();
var cacheExpiration = expiration.AddSeconds(-_options.Client.CacheLifetimeBuffer);
var data = $"{accessToken}___{expirationEpoch.ToString()}";
var entryOptions = new DistributedCacheEntryOptions
{
AbsoluteExpiration = cacheExpiration

The expireIn comes from the JWT expiration date. If this code executes to get the JWT and the JWT is going to expire less than the cache buffer for early expiration then the expiration date is in the past.

Now when I set CacheLifetimeBuffer to 0 I'm using that token until the very last moment. It is not ideal but this is how Adobe Marketo Oauth token route works. I would still like to have other oauth clients to have a cache lifetime buffer but this is for all clients and not specific clients.

@coolhome

So, if I understand this right, the token might expire 30 seconds from now, but because the CacheLifetimeBuffer is 60, its possible for the token to be cached even though it will expire BEFORE the cache does right?

What I would advise for now: implement your own IClientAccessTokenCache, maybe create a decorator for the default implementation, but either way in your SetAsync you can inject your own options, and pull the CacheLifetimeBuffer from it.

IE:

{
    "Clients": [
        {
            "Name": "AdobeMarketo",
            "CacheLifetimeBuffer": 0
        }
    ]
}
public class CustomTokenCacheOptions
{
    public List<CustomTokenCacheClientOptions> Clients { get; set; }
}

public class CustomTokenCacheClientOptions
{
    public string Name { get; set; }
    public int CacheLifetimeBuffer { get; set; }
}

Finally

public class CustomClientAccessTokenCache : IClientAccessTokenCache
{
        public ClientAccessTokenCache(IDistributedCache cache, IOptions<AccessTokenManagementOptions> options,  IOptions<CustomTokenCacheOptions > customOptions, ILogger<ClientAccessTokenCache> logger)
        {
            _cache = cache;
            _logger = logger;
            _options = options.Value;
            _customOptions = customOptions.Value;
        }

        public async Task SetAsync(string clientName, string accessToken, int expiresIn) 
        { 
            var optsForClient = _customOptions.Single(x => clientName == x.Name);

        }

}

@VictorioBerra I think that something like that may be my workaround solution.

Do you think there is value in implementing something like CacheLifetimeBuffer (Default, current value) and a nullable CacheLifetimeBuffer per client?

@VictorioBerra I think that something like that may be my workaround solution.

Do you think there is value in implementing something like CacheLifetimeBuffer (Default, current value) and a nullable CacheLifetimeBuffer per client?

You mean implementing the above solution into this project? Yeah I think if you can have multiple named clients you should allow them to be configured uniquely and not one size fits all but I will say 90% of people are probably calling a single API and most OAuth2 servers give you a shiny new token when you ask for one and not an old one. That implies they are storing them and I am not sure that desired.

Sorry - I did not follow the discussion. Was there any conclusion?

@leastprivilege I believe the conclusion is that it is a useful feature to configure the cache lifetime times per registered client given each client may be a different IdP. I see this more of an edge case for certain providers. @VictorioBerra any comment?

I agree, I do think its an edge case as well. If the effort to implement is low I could see it being useful.

Closing this discussion. If you think this would be a useful feature, please open a feature request with description how it should work from your POV. thanks!

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue.