aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Libs/README.md3
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs174
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs2
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs20
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs5
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs29
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs4
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs28
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs3
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs91
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs59
-rw-r--r--Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj2
-rw-r--r--Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs6
-rw-r--r--Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs3
-rw-r--r--Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs1
15 files changed, 277 insertions, 153 deletions
diff --git a/Libs/README.md b/Libs/README.md
new file mode 100644
index 0000000..db899cb
--- /dev/null
+++ b/Libs/README.md
@@ -0,0 +1,3 @@
+## VNLib.Plugins.Essentials.Sessions Helper libraries
+
+This folder contains helper libraries for the developing session providers. \ No newline at end of file
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs
index 271328a..c87c761 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs
@@ -1,16 +1,21 @@
using System;
using System.Net;
+using System.Text.Json;
using VNLib.Utils.Memory;
+using VNLib.Hashing.IdentityUtility;
using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Oauth.Applications;
using VNLib.Plugins.Essentials.Extensions;
-using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Sql;
-
+using VNLib.Plugins.Extensions.Validation;
namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints
{
+ delegate Task<IOAuth2TokenResult?> CreateTokenImpl(HttpEntity ev, UserApplication application, CancellationToken cancellation = default);
+
/// <summary>
/// Grants authorization to OAuth2 clients to protected resources
/// with access tokens
@@ -18,9 +23,11 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints
internal sealed class AccessTokenEndpoint : ResourceEndpointBase
{
- private readonly Lazy<ITokenManager> TokenStore;
+ private readonly CreateTokenImpl CreateToken;
private readonly Applications Applications;
+ private readonly Task<JsonDocument?> JWTVerificationKey;
+
//override protection settings to allow most connections to authenticate
protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
{
@@ -29,12 +36,14 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints
VerifySessionCors = false
};
- public AccessTokenEndpoint(string path, PluginBase pbase, Lazy<ITokenManager> tokenStore)
+ public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore, Task<JsonDocument?> verificationKey)
{
InitPathAndLog(path, pbase.Log);
- TokenStore = tokenStore;
+ CreateToken = tokenStore;
Applications = new(pbase.GetContextOptions(), pbase.GetPasswords());
+ JWTVerificationKey = verificationKey;
}
+
protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
{
@@ -43,66 +52,117 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints
{
//process a refresh token
}
- //Check for grant_type parameter from the request body
- if (!entity.RequestArgs.IsArgumentSet("grant_type", "client_credentials"))
- {
- entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type");
- //Default to bad request
- return VfReturnType.VirtualSkip;
- }
- //Get client id and secret (and make sure theyre not empty
- if (entity.RequestArgs.TryGetNonEmptyValue("client_id", out string? clientId) &&
- entity.RequestArgs.TryGetNonEmptyValue("client_secret", out string? secret))
+
+ //See if we have an application authorized with JWT
+ else if (entity.RequestArgs.IsArgumentSet("grant_type", "application"))
{
- if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(clientId))
- {
- //respond with error message
- entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_id");
- return VfReturnType.VirtualSkip;
- }
- if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(secret))
+ if(entity.RequestArgs.TryGetNonEmptyValue("token", out string? appJwt))
{
- //respond with error message
- entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_secret");
- return VfReturnType.VirtualSkip;
+ //Try to get and verify the app
+ UserApplication? app = GetApplicationFromJwt(appJwt);
+
+ //generate token
+ return await GenerateTokenAsync(entity, app);
}
- //Convert the clientid and secret to lowercase
- clientId = clientId.ToLower();
- secret = secret.ToLower();
- //Convert secret to private string
- PrivateString secretPv = new(secret, false);
- //Get the application from apps store
- UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv);
- if (app == null)
- {
- //App was not found or the credentials do not match
- entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The client credentials are invalid");
- return VfReturnType.VirtualSkip;
- }
- //Create a new session
- IOAuth2TokenResult? result = await TokenStore.Value.CreateAccessTokenAsync(entity.Entity, app, entity.EventCancellation);
- if (result == null)
+ }
+
+ //Check for grant_type parameter from the request body
+ else if (entity.RequestArgs.IsArgumentSet("grant_type", "client_credentials"))
+ {
+ //Get client id and secret (and make sure theyre not empty
+ if (entity.RequestArgs.TryGetNonEmptyValue("client_id", out string? clientId) &&
+ entity.RequestArgs.TryGetNonEmptyValue("client_secret", out string? secret))
{
- entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application");
- return VfReturnType.VirtualSkip;
+
+ if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(clientId))
+ {
+ //respond with error message
+ entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_id");
+ return VfReturnType.VirtualSkip;
+ }
+ if (!ValidatorExtensions.OnlyAlphaNumRegx.IsMatch(secret))
+ {
+ //respond with error message
+ entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_secret");
+ return VfReturnType.VirtualSkip;
+ }
+
+ //Convert the clientid and secret to lowercase
+ clientId = clientId.ToLower();
+ secret = secret.ToLower();
+
+ //Convert secret to private string that is unreferrenced
+ PrivateString secretPv = new(secret, false);
+
+ //Get the application from apps store
+ UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv);
+
+ return await GenerateTokenAsync(entity, app);
}
- //Create the new response message
- OauthTokenResponseMessage tokenMessage = new()
- {
- AccessToken = result.AccessToken,
-
- //set expired as seconds in int form
- Expires = result.ExpiresSeconds,
- RefreshToken = result.RefreshToken,
- TokenType = result.TokenType
- };
- //Respond with the token message
- entity.CloseResponseJson(HttpStatusCode.OK, tokenMessage);
+ }
+
+ entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type");
+ //Default to bad request
+ return VfReturnType.VirtualSkip;
+ }
+
+ private UserApplication? GetApplicationFromJwt(string jwtData)
+ {
+ //Not enabled
+ if (JWTVerificationKey.Result == null)
+ {
+ return null;
+ }
+
+ //Parse application token
+ using JsonWebToken jwt = JsonWebToken.Parse(jwtData);
+
+ //verify the application jwt
+ if (!jwt.VerifyFromJwk(JWTVerificationKey.Result.RootElement))
+ {
+ return null;
+ }
+
+ using JsonDocument doc = jwt.GetPayload();
+
+ //Get expiration time
+ DateTimeOffset exp = doc.RootElement.GetProperty("exp").GetDateTimeOffset();
+
+ //Check if token is expired
+ return exp < DateTimeOffset.UtcNow ? null : UserApplication.FromJwtDoc(doc.RootElement);
+ }
+
+
+ private async Task<VfReturnType> GenerateTokenAsync(HttpEntity entity, UserApplication? app)
+ {
+ if (app == null)
+ {
+ //App was not found or the credentials do not match
+ entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The credentials are invalid or do not exist");
return VfReturnType.VirtualSkip;
+ }
+ IOAuth2TokenResult? result = await CreateToken(entity, app, entity.EventCancellation);
+
+ if (result == null)
+ {
+ entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application");
+ return VfReturnType.VirtualSkip;
}
- //respond with error message
- entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The request was missing required arguments");
+
+ //Create the new response message
+ OauthTokenResponseMessage tokenMessage = new()
+ {
+ AccessToken = result.AccessToken,
+ IdToken = result.IdentityToken,
+ //set expired as seconds in int form
+ Expires = result.ExpiresSeconds,
+ RefreshToken = result.RefreshToken,
+ TokenType = result.TokenType
+ };
+
+ //Respond with the token message
+ entity.CloseResponseJson(HttpStatusCode.OK, tokenMessage);
return VfReturnType.VirtualSkip;
}
}
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs
index 9a65d62..9eb9928 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs
@@ -1,7 +1,7 @@
using System;
using VNLib.Net.Http;
-using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Essentials.Oauth.Applications;
using VNLib.Plugins.Sessions.Cache.Client;
namespace VNLib.Plugins.Essentials.Sessions.OAuth
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs
index e15c6e4..89b36ad 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs
@@ -5,6 +5,7 @@ using VNLib.Net.Http;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Essentials.Oauth.Applications;
using VNLib.Plugins.Essentials.Sessions.OAuth;
using VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints;
using VNLib.Plugins.Extensions.Loading;
@@ -43,11 +44,12 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth
string tokenEpPath = oauth2Config["token_path"].GetString() ?? throw new KeyNotFoundException($"Missing required 'token_path' in '{OAUTH2_CONFIG_KEY}' config");
- //TODO fix with method that will wait until cache is actually loaded
- Lazy<ITokenManager> lazyTokenMan = new(() => _sessions!, false);
+ //Optional application jwt token
+ Task<JsonDocument?> jwtTokenSecret = plugin.TryGetSecretAsync("application_token_key")
+ .ContinueWith(static t => t.Result == null ? null : JsonDocument.Parse(t.Result));
//Init auth endpoint
- AccessTokenEndpoint authEp = new(tokenEpPath, plugin, lazyTokenMan);
+ AccessTokenEndpoint authEp = new(tokenEpPath, plugin, CreateTokenDelegateAsync, jwtTokenSecret);
//route auth endpoint
plugin.Route(authEp);
@@ -56,14 +58,21 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth
plugin.Route<RevocationEndpoint>();
//Run
- _ = WokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config);
+ _ = CacheWokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config);
+ }
+
+ private async Task<IOAuth2TokenResult?> CreateTokenDelegateAsync(HttpEntity entity, UserApplication app, CancellationToken cancellation)
+ {
+ return await _sessions!.CreateAccessTokenAsync(entity, app, cancellation).ConfigureAwait(false);
}
/*
* Starts and monitors the VNCache connection
*/
- private async Task WokerDoWorkAsync(PluginBase plugin, ILogProvider localized, IReadOnlyDictionary<string, JsonElement> cacheConfig, IReadOnlyDictionary<string, JsonElement> oauth2Config)
+ private async Task CacheWokerDoWorkAsync(PluginBase plugin, ILogProvider localized,
+ IReadOnlyDictionary<string, JsonElement> cacheConfig,
+ IReadOnlyDictionary<string, JsonElement> oauth2Config)
{
//Init cache client
using VnCacheClient cache = new(plugin.IsDebug() ? plugin.Log : null, Utils.Memory.Memory.Shared);
@@ -89,7 +98,6 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth
//Schedule cleanup interval with the plugin scheduler
plugin.ScheduleInterval(_sessions, cleanupInterval);
-
localized.Information("Session provider loaded");
//Run and wait for exit
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs
index 2b23721..1058653 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs
@@ -1,12 +1,11 @@
using System;
using System.Diagnostics.CodeAnalysis;
-using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Utils.Extensions;
using VNLib.Hashing;
using VNLib.Net.Http;
-using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Essentials.Oauth.Applications;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Sessions.Cache.Client;
using static VNLib.Plugins.Essentials.Oauth.OauthSessionExtensions;
@@ -90,7 +89,7 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth
string sessionId = ComputeSessionIdFromToken(token);
//Clear buffer
- Utils.Memory.Memory.InitializeBlock(mem.Span);
+ Memory.InitializeBlock(mem.Span);
//Return sessid result
return new(sessionId, token, null);
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs
index d938641..79d3789 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs
@@ -9,9 +9,10 @@ using VNLib.Net.Http;
using VNLib.Data.Caching;
using VNLib.Data.Caching.Exceptions;
using VNLib.Net.Messaging.FBM.Client;
+using VNLib.Plugins.Sessions.Cache.Client;
using VNLib.Plugins.Essentials.Oauth;
using VNLib.Plugins.Essentials.Oauth.Tokens;
-using VNLib.Plugins.Sessions.Cache.Client;
+using VNLib.Plugins.Essentials.Oauth.Applications;
using VNLib.Plugins.Extensions.Loading.Events;
namespace VNLib.Plugins.Essentials.Sessions.OAuth
@@ -24,8 +25,10 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth
{
private static readonly SessionHandle NotFoundHandle = new(null, FileProcessArgs.NotFound, null);
- static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10);
+
+ private static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10);
+
private readonly IOauthSessionIdFactory factory;
private readonly TokenStore TokenStore;
@@ -103,26 +106,26 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth
}
}
///<inheritdoc/>
- async Task<IOAuth2TokenResult?> ITokenManager.CreateAccessTokenAsync(IHttpEvent ev, UserApplication app, CancellationToken cancellation)
+ public async Task<IOAuth2TokenResult?> CreateAccessTokenAsync(HttpEntity ev, UserApplication app, CancellationToken cancellation)
{
//Get a new session for the current connection
TokenAndSessionIdResult ids = factory.GenerateTokensAndId();
//try to insert token into the store, may fail if max has been reached
- if (await TokenStore.InsertTokenAsync(ids.SessionId, app.Id, ids.RefreshToken, factory.MaxTokensPerApp, cancellation) != ERRNO.SUCCESS)
+ if (await TokenStore.InsertTokenAsync(ids.SessionId, app.Id!, ids.RefreshToken, factory.MaxTokensPerApp, cancellation) != ERRNO.SUCCESS)
{
return null;
}
//Create new session from the session id
RemoteSession session = SessionCtor(ids.SessionId);
- await session.WaitAndLoadAsync(ev, cancellation);
+ await session.WaitAndLoadAsync(ev.Entity, cancellation);
try
{
//Init new session
- factory.InitNewSession(session, app, ev);
+ factory.InitNewSession(session, app, ev.Entity);
}
finally
{
- await session.UpdateAndRelease(false, ev);
+ await session.UpdateAndRelease(false, ev.Entity);
}
//Init new token result to pass to client
return new OAuth2TokenResult()
@@ -137,14 +140,20 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth
///<inheritdoc/>
Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation)
{
- throw new NotImplementedException();
+ return TokenStore.RevokeTokensAsync(tokens, cancellation);
}
///<inheritdoc/>
Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation)
{
- throw new NotImplementedException();
+ return TokenStore.RevokeTokenAsync(appId, cancellation);
}
-
+
+
+ /*
+ * Interval for remving expired tokens
+ */
+
+ ///<inheritdoc/>
async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken)
{
//Calculate valid token time
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs
index fd725bf..bf2b28e 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions.VNCache/WebSessionProvider.cs
@@ -109,6 +109,10 @@ namespace VNLib.Plugins.Essentials.Sessions.VNCache
}
return new SessionHandle(session, HandleClosedAsync);
}
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
catch (SessionException)
{
throw;
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs
index 35e2fea..4e09b04 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySession.cs
@@ -16,15 +16,16 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
{
private readonly Dictionary<string, string> DataStorage;
- private readonly MemorySessionStore SessionStore;
+ private readonly Func<IHttpEvent, string, string> OnSessionUpdate;
- public MemorySession(IPAddress ipAddress, MemorySessionStore SessionStore)
+ public MemorySession(string sessionId, IPAddress ipAddress, Func<IHttpEvent, string, string> onSessionUpdate)
{
//Set the initial is-new flag
DataStorage = new Dictionary<string, string>(10);
- this.SessionStore = SessionStore;
+
+ OnSessionUpdate = onSessionUpdate;
//Get new session id
- SessionID = SessionStore.NewSessionID;
+ SessionID = sessionId;
UserIP = ipAddress;
SessionType = SessionType.Web;
Created = DateTimeOffset.UtcNow;
@@ -45,7 +46,8 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
{
//Clear storage, and regenerate the sessionid
DataStorage.Clear();
- RegenId(state);
+ //store new sessionid
+ SessionID = OnSessionUpdate(state, SessionID);
//Reset ip-address
UserIP = state.Server.GetTrustedIp();
//Update created-time
@@ -58,26 +60,14 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
else if (Flags.IsSet(REGEN_ID_MSK))
{
//Regen id without modifying the data store
- RegenId(state);
+ SessionID = OnSessionUpdate(state, SessionID);
}
//Clear flags
Flags.ClearAll();
//Memory session always completes
return ValueTask.FromResult<Task?>(null);
}
-
- private void RegenId(IHttpEvent entity)
- {
- //Get a new session-id
- string newId = SessionStore.NewSessionID;
- //Update the cache entry
- SessionStore.UpdateRecord(newId, this);
- //store new sessionid
- SessionID = newId;
- //set cookie
- SessionStore.SetSessionCookie(entity, this);
- }
-
+
protected override Task OnEvictedAsync()
{
//Clear all session data
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs
index f41d384..df5dd59 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionEntrypoint.cs
@@ -52,6 +52,9 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
//Schedule garbage collector
_ = plugin.ScheduleInterval(this, TimeSpan.FromMinutes(1));
+
+ //Call cleanup on exit
+ _ = plugin.UnloadToken.RegisterUnobserved(_sessions.Cleanup);
}
Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken)
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs
index 15c3002..388f998 100644
--- a/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/MemorySessionStore.cs
@@ -3,7 +3,6 @@ using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
-using VNLib.Hashing;
using VNLib.Net.Http;
using VNLib.Net.Sessions;
using VNLib.Utils;
@@ -11,10 +10,10 @@ using VNLib.Utils.Async;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Extensions;
+#nullable enable
namespace VNLib.Plugins.Essentials.Sessions.Memory
{
-
/// <summary>
/// An <see cref="ISessionProvider"/> for in-process-memory backed sessions
/// </summary>
@@ -23,11 +22,13 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
private readonly Dictionary<string, MemorySession> SessionsStore;
internal readonly MemorySessionConfig Config;
+ internal readonly SessionIdFactory IdFactory;
public MemorySessionStore(MemorySessionConfig config)
{
Config = config;
SessionsStore = new(config.MaxAllowedSessions, StringComparer.Ordinal);
+ IdFactory = new(config.SessionIdSizeBytes, config.SessionCookieID, config.SessionTimeout);
}
///<inheritdoc/>
@@ -36,11 +37,11 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
static ValueTask SessionHandleClosedAsync(ISession session, IHttpEvent ev)
{
- return (session as MemorySession).UpdateAndRelease(true, ev);
+ return (session as MemorySession)!.UpdateAndRelease(true, ev);
}
- //Check for previous session cookie
- if (entity.Server.RequestCookies.TryGetNonEmptyValue(Config.SessionCookieID, out string sessionId))
+ //Try to get the id for the session
+ if (IdFactory.TryGetSessionId(entity, out string? sessionId))
{
//Try to get the old record or evict it
ERRNO result = SessionsStore.TryGetOrEvictRecord(sessionId, out MemorySession session);
@@ -50,63 +51,51 @@ namespace VNLib.Plugins.Essentials.Sessions.Memory
await session.WaitOneAsync(cancellationToken);
return new (session, SessionHandleClosedAsync);
}
- //Continue creating a new session
+ else
+ {
+ //try to cleanup expired records
+ GC();
+ //Make sure there is enough room to add a new session
+ if (SessionsStore.Count >= Config.MaxAllowedSessions)
+ {
+ entity.Server.SetNoCache();
+ //Set 503 when full
+ entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable);
+ //Cannot service new session
+ return new(null, FileProcessArgs.VirtualSkip, null);
+ }
+ //Initialze a new session
+ session = new(sessionId, entity.Server.GetTrustedIp(), UpdateSessionId);
+ //Increment the semaphore
+ (session as IWaitHandle).WaitOne();
+ //store the session in cache while holding semaphore, and set its expiration
+ SessionsStore.StoreRecord(session.SessionID, session, Config.SessionTimeout);
+ //Init new session handle
+ return new (session, SessionHandleClosedAsync);
+ }
}
-
- //Dont service non browsers for new sessions
- if (!entity.Server.IsBrowser())
+ else
{
return SessionHandle.Empty;
}
-
- //try to cleanup expired records
- SessionsStore.CollectRecords();
- //Make sure there is enough room to add a new session
- if (SessionsStore.Count >= Config.MaxAllowedSessions)
- {
- entity.Server.SetNoCache();
- //Set 503 when full
- entity.CloseResponse(System.Net.HttpStatusCode.ServiceUnavailable);
- //Cannot service new session
- return new(null, FileProcessArgs.VirtualSkip, null);
- }
- //Initialze a new session
- MemorySession ms = new(entity.Server.GetTrustedIp(), this);
- //Set session cookie
- SetSessionCookie(entity, ms);
- //Increment the semaphore
- (ms as IWaitHandle).WaitOne();
- //store the session in cache while holding semaphore, and set its expiration
- SessionsStore.StoreRecord(ms.SessionID, ms, Config.SessionTimeout);
- //Init new session handle
- return new SessionHandle(ms, SessionHandleClosedAsync);
}
- /// <summary>
- /// Gets a new unique sessionid for sessions
- /// </summary>
- internal string NewSessionID => RandomHash.GetRandomHex((int)Config.SessionIdSizeBytes);
-
- internal void UpdateRecord(string newSessId, MemorySession session)
+ private string UpdateSessionId(IHttpEvent entity, string oldId)
{
+ //Generate and set a new sessionid
+ string newid = IdFactory.GenerateSessionId(entity);
+ //Aquire lock on cache
lock (SessionsStore)
{
- //Remove old record from the store
- SessionsStore.Remove(session.SessionID);
- //Insert the new session
- SessionsStore.Add(newSessId, session);
+ //Change the cache lookup id
+ if (SessionsStore.Remove(oldId, out MemorySession? session))
+ {
+ SessionsStore.Add(newid, session);
+ }
}
+ return newid;
}
- /// <summary>
- /// Sets a standard session cookie for an entity/connection
- /// </summary>
- /// <param name="entity">The entity to set the cookie on</param>
- /// <param name="session">The session attached to the </param>
- internal void SetSessionCookie(IHttpEvent entity, MemorySession session)
- {
- //Set session cookie
- entity.Server.SetCookie(Config.SessionCookieID, session.SessionID, null, "/", Config.SessionTimeout, CookieSameSite.Lax, true, true);
- }
+
/// <summary>
/// Evicts all sessions from the current store
/// </summary>
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs
new file mode 100644
index 0000000..ff0608e
--- /dev/null
+++ b/Libs/VNLib.Plugins.Essentials.Sessions/SessionIdFactory.cs
@@ -0,0 +1,59 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Hashing;
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Extensions;
+
+#nullable enable
+
+namespace VNLib.Plugins.Essentials.Sessions.Memory
+{
+ internal sealed class SessionIdFactory : ISessionIdFactory
+ {
+ private readonly int IdSize;
+ private readonly string cookieName;
+ private readonly TimeSpan ValidFor;
+
+ public SessionIdFactory(uint idSize, string cookieName, TimeSpan validFor)
+ {
+ IdSize = (int)idSize;
+ this.cookieName = cookieName;
+ ValidFor = validFor;
+ }
+
+ public string GenerateSessionId(IHttpEvent entity)
+ {
+ //Random hex hash
+ string cookie = RandomHash.GetRandomBase32(IdSize);
+
+ //Set the session id cookie
+ entity.Server.SetCookie(cookieName, cookie, ValidFor, secure: true, httpOnly: true);
+
+ //return session-id value from cookie value
+ return cookie;
+ }
+
+ public bool TryGetSessionId(IHttpEvent entity, [NotNullWhen(true)] out string? sessionId)
+ {
+ //Get authorization token and make sure its not too large to cause a buffer overflow
+ if (entity.Server.GetCookie(cookieName, out sessionId))
+ {
+ return true;
+ }
+ //Only add sessions for user-agents
+ else if (entity.Server.IsBrowser())
+ {
+ //Get a new session id
+ sessionId = GenerateSessionId(entity);
+
+ return true;
+ }
+ else
+ {
+ sessionId = null;
+ return false;
+ }
+ }
+ }
+}
diff --git a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj
index 0c12cec..52404be 100644
--- a/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj
+++ b/Libs/VNLib.Plugins.Sessions.Cache.Client/VNLib.Plugins.Sessions.Cache.Client.csproj
@@ -21,7 +21,7 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
- <PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.44" />
+ <PackageReference Include="Microsoft.VisualStudio.Threading" Version="17.3.48" />
<PackageReference Include="RestSharp" Version="108.0.2" />
</ItemGroup>
diff --git a/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs b/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs
index cdc0dc1..06ebfc3 100644
--- a/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs
+++ b/Plugins/CacheBroker/Endpoints/BrokerRegistrationEndpoint.cs
@@ -22,13 +22,13 @@ using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
using VNLib.Hashing.IdentityUtility;
using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Events;
using VNLib.Plugins.Extensions.Loading.Configuration;
using VNLib.Net.Rest.Client;
-
#nullable enable
namespace VNLib.Plugins.Cache.Broker.Endpoints
@@ -188,7 +188,7 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints
//Copy input stream to buffer
await inputStream.CopyToAsync(buffer, 4096, Memory.Shared);
//Parse jwt
- return JsonWebToken.Parse(buffer.AsSpan());
+ return JsonWebToken.ParseRaw(buffer.AsSpan());
}
protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity)
@@ -200,7 +200,7 @@ namespace VNLib.Plugins.Cache.Broker.Endpoints
{
alg.ImportSubjectPublicKeyInfo(CachePubKey.Result, out _);
//Verify the jwt
- if (!jwt.Verify(alg, SignatureHashAlg))
+ if (!jwt.Verify(alg, in SignatureHashAlg))
{
entity.CloseResponse(HttpStatusCode.Unauthorized);
return VfReturnType.VirtualSkip;
diff --git a/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs b/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs
index e80be77..d501fca 100644
--- a/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs
+++ b/Plugins/SessionCacheServer/Endpoints/BrokerHeartBeat.cs
@@ -9,11 +9,10 @@ using System.Security.Cryptography;
using VNLib.Data.Caching.Extensions;
using VNLib.Hashing.IdentityUtility;
+using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Loading;
-#nullable enable
-
namespace VNLib.Plugins.Essentials.Sessions.Server.Endpoints
{
internal class BrokerHeartBeat : ResourceEndpointBase
diff --git a/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs b/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs
index fc4de30..0385601 100644
--- a/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs
+++ b/Plugins/SessionCacheServer/Endpoints/ConnectEndpoint.cs
@@ -18,6 +18,7 @@ using VNLib.Net.Messaging.FBM.Server;
using VNLib.Data.Caching.Extensions;
using VNLib.Data.Caching.ObjectCache;
using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Extensions;