diff --git a/src/SenseNet.Client/Authentication/TokenStore.cs b/src/SenseNet.Client/Authentication/TokenStore.cs index af77f83..47072d6 100644 --- a/src/SenseNet.Client/Authentication/TokenStore.cs +++ b/src/SenseNet.Client/Authentication/TokenStore.cs @@ -29,9 +29,11 @@ Task GetTokenAsync(ServerContext server, string clientId, string secret, internal class TokenStore : ITokenStore { private readonly ILogger _logger; - private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions()); + private readonly MemoryCache _cache = new(new MemoryCacheOptions()); private readonly ITokenProvider _tokenProvider; + private const int DefaultCacheDurationInMinutes = 30; + public TokenStore(ITokenProvider tokenProvider, ILogger logger) { _tokenProvider = tokenProvider; @@ -42,12 +44,12 @@ public async Task GetTokenAsync(ServerContext server, string clientId, s CancellationToken cancel = default) { // look for the token in the cache - var tokenCacheKey = "TK:" + server.Url; + var tokenCacheKey = $"TK-{server.Url}-{clientId}".GetHashCode(); if (_cache.TryGetValue(tokenCacheKey, out string accessToken)) return accessToken; // look for auth info in the cache - var authInfoCacheKey = "AI:" + server.Url; + var authInfoCacheKey = $"AI-{server.Url}-{clientId}".GetHashCode(); if (!_cache.TryGetValue(authInfoCacheKey, out AuthorityInfo authInfo)) { _logger?.LogTrace($"Getting authority info from {server.Url}."); @@ -59,7 +61,7 @@ public async Task GetTokenAsync(ServerContext server, string clientId, s authInfo.ClientId = clientId; if (!string.IsNullOrEmpty(authInfo.Authority)) - _cache.Set(authInfoCacheKey, authInfo, TimeSpan.FromMinutes(30)); + _cache.Set(authInfoCacheKey, authInfo, TimeSpan.FromMinutes(DefaultCacheDurationInMinutes)); else _logger?.LogTrace($"Authority info is empty for server {server.Url}"); } @@ -74,24 +76,28 @@ public async Task GetTokenAsync(ServerContext server, string clientId, s accessToken = tokenInfo?.AccessToken; - //TODO: determine access token cache expiration based on the token expiration - if (!string.IsNullOrEmpty(accessToken)) + if (string.IsNullOrEmpty(accessToken)) + return accessToken; + + // Maximize token expiration to the received token expiration (if given) or a fixed short time. + var tokenExpiration = tokenInfo.ExpiresIn > 0 + ? TimeSpan.FromSeconds(Math.Min(tokenInfo.ExpiresIn, DefaultCacheDurationInMinutes * 60)) + : TimeSpan.FromMinutes(DefaultCacheDurationInMinutes); + + _cache.Set(tokenCacheKey, accessToken, tokenExpiration); + + try + { + // parse token and log user + var handler = new JwtSecurityTokenHandler(); + var token = handler.ReadJwtToken(accessToken); + var sub = token.Claims.FirstOrDefault(c => c.Type == "client_sub")?.Value ?? "unknown"; + + _logger?.LogTrace($"Token acquired for user {sub}. Expires in {tokenExpiration}."); + } + catch (Exception ex) { - _cache.Set(tokenCacheKey, accessToken, TimeSpan.FromMinutes(10)); - - try - { - // parse token and log user - var handler = new JwtSecurityTokenHandler(); - var token = handler.ReadJwtToken(accessToken); - var sub = token.Claims.FirstOrDefault(c => c.Type == "client_sub")?.Value ?? "unknown"; - - _logger?.LogTrace($"Token acquired for user {sub}."); - } - catch (Exception ex) - { - _logger?.LogWarning(ex, $"Could not read access token: {ex.Message}"); - } + _logger?.LogWarning(ex, $"Could not read access token: {ex.Message}"); } return accessToken; diff --git a/src/SenseNet.Client/Repository/RepositoryCollection.cs b/src/SenseNet.Client/Repository/RepositoryCollection.cs index b34d130..dee6dcc 100644 --- a/src/SenseNet.Client/Repository/RepositoryCollection.cs +++ b/src/SenseNet.Client/Repository/RepositoryCollection.cs @@ -16,6 +16,8 @@ internal class RepositoryCollection : IRepositoryCollection private readonly MemoryCache _repositories = new(new MemoryCacheOptions { SizeLimit = 1024 }); private readonly SemaphoreSlim _asyncLock = new(1, 1); + private const int DefaultCacheDurationInMinutes = 30; + public RepositoryCollection(IServiceProvider services, IServerContextFactory serverFactory, ILogger logger) { _services = services; @@ -67,7 +69,7 @@ int GetCacheKey() _repositories.Set(cacheKey, repo, new MemoryCacheEntryOptions { - AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.AddHours(1)), + AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.AddMinutes(DefaultCacheDurationInMinutes)), Size = 1 }); diff --git a/src/SenseNet.Client/ServerContextFactory.cs b/src/SenseNet.Client/ServerContextFactory.cs index 19a2215..f9903c2 100644 --- a/src/SenseNet.Client/ServerContextFactory.cs +++ b/src/SenseNet.Client/ServerContextFactory.cs @@ -1,6 +1,8 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SenseNet.Client.Authentication; @@ -44,7 +46,9 @@ internal class ServerContextFactory : IServerContextFactory private readonly ServerContextOptions _serverContextOptions; private readonly RepositoryOptions _repositoryOptions; private readonly SemaphoreSlim _asyncLock = new(1, 1); - private readonly IDictionary _servers = new Dictionary(); + private readonly MemoryCache _servers = new(new MemoryCacheOptions()); + + private const int DefaultCacheDurationInMinutes = 30; public ServerContextFactory(ITokenStore tokenStore, IOptions serverContextOptions, IOptions repositoryOptions, ILogger logger) @@ -63,20 +67,20 @@ ServerContext CloneWithToken(ServerContext original) var clonedServer = original.Clone(); // set token only if provided, do not overwrite cached value if no token is present - if (!string.IsNullOrEmpty(token)) - { - clonedServer.Authentication.AccessToken = token; + if (string.IsNullOrEmpty(token)) + return clonedServer; + + clonedServer.Authentication.AccessToken = token; - // if a token is provided, do NOT use the configured api key to prevent a security issue - clonedServer.Authentication.ApiKey = null; - } + // if a token is provided, do NOT use the configured api key to prevent a security issue + clonedServer.Authentication.ApiKey = null; return clonedServer; } name ??= ServerContextOptions.DefaultServerName; - if (_servers.TryGetValue(name, out var server)) + if (_servers.TryGetValue(name, out ServerContext server)) return CloneWithToken(server); await _asyncLock.WaitAsync(); @@ -86,11 +90,14 @@ ServerContext CloneWithToken(ServerContext original) if (_servers.TryGetValue(name, out server)) return CloneWithToken(server); + _logger.LogTrace("Constructing a new server instance " + + $"for {(string.IsNullOrWhiteSpace(name) ? "the default server" : name)}"); + // cache the authenticated server server = await GetAuthenticatedServerAsync(name).ConfigureAwait(false); if (server != null) - _servers[name] = server; + _servers.Set(name, server, TimeSpan.FromMinutes(DefaultCacheDurationInMinutes)); } finally {