Skip to content

Commit

Permalink
Cache servers and repos only for a short time to let the component re…
Browse files Browse the repository at this point in the history
…fresh the token automatically. (#120)
  • Loading branch information
tusmester authored Aug 31, 2023
1 parent d66dee9 commit a9d1c9e
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 32 deletions.
48 changes: 27 additions & 21 deletions src/SenseNet.Client/Authentication/TokenStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ Task<string> GetTokenAsync(ServerContext server, string clientId, string secret,
internal class TokenStore : ITokenStore
{
private readonly ILogger<TokenStore> _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<TokenStore> logger)
{
_tokenProvider = tokenProvider;
Expand All @@ -42,12 +44,12 @@ public async Task<string> 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}.");
Expand All @@ -59,7 +61,7 @@ public async Task<string> 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}");
}
Expand All @@ -74,24 +76,28 @@ public async Task<string> 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;
Expand Down
4 changes: 3 additions & 1 deletion src/SenseNet.Client/Repository/RepositoryCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<RepositoryCollection> logger)
{
_services = services;
Expand Down Expand Up @@ -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
});

Expand Down
27 changes: 17 additions & 10 deletions src/SenseNet.Client/ServerContextFactory.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<string, ServerContext> _servers = new Dictionary<string, ServerContext>();
private readonly MemoryCache _servers = new(new MemoryCacheOptions());

private const int DefaultCacheDurationInMinutes = 30;

public ServerContextFactory(ITokenStore tokenStore, IOptions<ServerContextOptions> serverContextOptions,
IOptions<RepositoryOptions> repositoryOptions, ILogger<ServerContextFactory> logger)
Expand All @@ -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();
Expand All @@ -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
{
Expand Down

0 comments on commit a9d1c9e

Please sign in to comment.