Skip to content

Commit a9d1c9e

Browse files
authored
Cache servers and repos only for a short time to let the component refresh the token automatically. (#120)
1 parent d66dee9 commit a9d1c9e

3 files changed

Lines changed: 47 additions & 32 deletions

File tree

src/SenseNet.Client/Authentication/TokenStore.cs

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ Task<string> GetTokenAsync(ServerContext server, string clientId, string secret,
2929
internal class TokenStore : ITokenStore
3030
{
3131
private readonly ILogger<TokenStore> _logger;
32-
private readonly MemoryCache _cache = new MemoryCache(new MemoryCacheOptions());
32+
private readonly MemoryCache _cache = new(new MemoryCacheOptions());
3333
private readonly ITokenProvider _tokenProvider;
3434

35+
private const int DefaultCacheDurationInMinutes = 30;
36+
3537
public TokenStore(ITokenProvider tokenProvider, ILogger<TokenStore> logger)
3638
{
3739
_tokenProvider = tokenProvider;
@@ -42,12 +44,12 @@ public async Task<string> GetTokenAsync(ServerContext server, string clientId, s
4244
CancellationToken cancel = default)
4345
{
4446
// look for the token in the cache
45-
var tokenCacheKey = "TK:" + server.Url;
47+
var tokenCacheKey = $"TK-{server.Url}-{clientId}".GetHashCode();
4648
if (_cache.TryGetValue(tokenCacheKey, out string accessToken))
4749
return accessToken;
4850

4951
// look for auth info in the cache
50-
var authInfoCacheKey = "AI:" + server.Url;
52+
var authInfoCacheKey = $"AI-{server.Url}-{clientId}".GetHashCode();
5153
if (!_cache.TryGetValue(authInfoCacheKey, out AuthorityInfo authInfo))
5254
{
5355
_logger?.LogTrace($"Getting authority info from {server.Url}.");
@@ -59,7 +61,7 @@ public async Task<string> GetTokenAsync(ServerContext server, string clientId, s
5961
authInfo.ClientId = clientId;
6062

6163
if (!string.IsNullOrEmpty(authInfo.Authority))
62-
_cache.Set(authInfoCacheKey, authInfo, TimeSpan.FromMinutes(30));
64+
_cache.Set(authInfoCacheKey, authInfo, TimeSpan.FromMinutes(DefaultCacheDurationInMinutes));
6365
else
6466
_logger?.LogTrace($"Authority info is empty for server {server.Url}");
6567
}
@@ -74,24 +76,28 @@ public async Task<string> GetTokenAsync(ServerContext server, string clientId, s
7476

7577
accessToken = tokenInfo?.AccessToken;
7678

77-
//TODO: determine access token cache expiration based on the token expiration
78-
if (!string.IsNullOrEmpty(accessToken))
79+
if (string.IsNullOrEmpty(accessToken))
80+
return accessToken;
81+
82+
// Maximize token expiration to the received token expiration (if given) or a fixed short time.
83+
var tokenExpiration = tokenInfo.ExpiresIn > 0
84+
? TimeSpan.FromSeconds(Math.Min(tokenInfo.ExpiresIn, DefaultCacheDurationInMinutes * 60))
85+
: TimeSpan.FromMinutes(DefaultCacheDurationInMinutes);
86+
87+
_cache.Set(tokenCacheKey, accessToken, tokenExpiration);
88+
89+
try
90+
{
91+
// parse token and log user
92+
var handler = new JwtSecurityTokenHandler();
93+
var token = handler.ReadJwtToken(accessToken);
94+
var sub = token.Claims.FirstOrDefault(c => c.Type == "client_sub")?.Value ?? "unknown";
95+
96+
_logger?.LogTrace($"Token acquired for user {sub}. Expires in {tokenExpiration}.");
97+
}
98+
catch (Exception ex)
7999
{
80-
_cache.Set(tokenCacheKey, accessToken, TimeSpan.FromMinutes(10));
81-
82-
try
83-
{
84-
// parse token and log user
85-
var handler = new JwtSecurityTokenHandler();
86-
var token = handler.ReadJwtToken(accessToken);
87-
var sub = token.Claims.FirstOrDefault(c => c.Type == "client_sub")?.Value ?? "unknown";
88-
89-
_logger?.LogTrace($"Token acquired for user {sub}.");
90-
}
91-
catch (Exception ex)
92-
{
93-
_logger?.LogWarning(ex, $"Could not read access token: {ex.Message}");
94-
}
100+
_logger?.LogWarning(ex, $"Could not read access token: {ex.Message}");
95101
}
96102

97103
return accessToken;

src/SenseNet.Client/Repository/RepositoryCollection.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ internal class RepositoryCollection : IRepositoryCollection
1616
private readonly MemoryCache _repositories = new(new MemoryCacheOptions { SizeLimit = 1024 });
1717
private readonly SemaphoreSlim _asyncLock = new(1, 1);
1818

19+
private const int DefaultCacheDurationInMinutes = 30;
20+
1921
public RepositoryCollection(IServiceProvider services, IServerContextFactory serverFactory, ILogger<RepositoryCollection> logger)
2022
{
2123
_services = services;
@@ -67,7 +69,7 @@ int GetCacheKey()
6769

6870
_repositories.Set(cacheKey, repo, new MemoryCacheEntryOptions
6971
{
70-
AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.AddHours(1)),
72+
AbsoluteExpiration = new DateTimeOffset(DateTime.UtcNow.AddMinutes(DefaultCacheDurationInMinutes)),
7173
Size = 1
7274
});
7375

src/SenseNet.Client/ServerContextFactory.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Threading;
34
using System.Threading.Tasks;
5+
using Microsoft.Extensions.Caching.Memory;
46
using Microsoft.Extensions.Logging;
57
using Microsoft.Extensions.Options;
68
using SenseNet.Client.Authentication;
@@ -44,7 +46,9 @@ internal class ServerContextFactory : IServerContextFactory
4446
private readonly ServerContextOptions _serverContextOptions;
4547
private readonly RepositoryOptions _repositoryOptions;
4648
private readonly SemaphoreSlim _asyncLock = new(1, 1);
47-
private readonly IDictionary<string, ServerContext> _servers = new Dictionary<string, ServerContext>();
49+
private readonly MemoryCache _servers = new(new MemoryCacheOptions());
50+
51+
private const int DefaultCacheDurationInMinutes = 30;
4852

4953
public ServerContextFactory(ITokenStore tokenStore, IOptions<ServerContextOptions> serverContextOptions,
5054
IOptions<RepositoryOptions> repositoryOptions, ILogger<ServerContextFactory> logger)
@@ -63,20 +67,20 @@ ServerContext CloneWithToken(ServerContext original)
6367
var clonedServer = original.Clone();
6468

6569
// set token only if provided, do not overwrite cached value if no token is present
66-
if (!string.IsNullOrEmpty(token))
67-
{
68-
clonedServer.Authentication.AccessToken = token;
70+
if (string.IsNullOrEmpty(token))
71+
return clonedServer;
72+
73+
clonedServer.Authentication.AccessToken = token;
6974

70-
// if a token is provided, do NOT use the configured api key to prevent a security issue
71-
clonedServer.Authentication.ApiKey = null;
72-
}
75+
// if a token is provided, do NOT use the configured api key to prevent a security issue
76+
clonedServer.Authentication.ApiKey = null;
7377

7478
return clonedServer;
7579
}
7680

7781
name ??= ServerContextOptions.DefaultServerName;
7882

79-
if (_servers.TryGetValue(name, out var server))
83+
if (_servers.TryGetValue(name, out ServerContext server))
8084
return CloneWithToken(server);
8185

8286
await _asyncLock.WaitAsync();
@@ -86,11 +90,14 @@ ServerContext CloneWithToken(ServerContext original)
8690
if (_servers.TryGetValue(name, out server))
8791
return CloneWithToken(server);
8892

93+
_logger.LogTrace("Constructing a new server instance " +
94+
$"for {(string.IsNullOrWhiteSpace(name) ? "the default server" : name)}");
95+
8996
// cache the authenticated server
9097
server = await GetAuthenticatedServerAsync(name).ConfigureAwait(false);
9198

9299
if (server != null)
93-
_servers[name] = server;
100+
_servers.Set(name, server, TimeSpan.FromMinutes(DefaultCacheDurationInMinutes));
94101
}
95102
finally
96103
{

0 commit comments

Comments
 (0)