aboutsummaryrefslogtreecommitdiff
path: root/libs/VNLib.Plugins.Sessions.OAuth/src
diff options
context:
space:
mode:
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.OAuth/src')
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs197
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs56
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs74
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/O2AuthenticationPluginEntry.cs58
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs120
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2Session.cs132
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs131
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionProvider.cs230
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenResult.cs37
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/OauthTokenResponseMessage.cs42
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs41
-rw-r--r--libs/VNLib.Plugins.Sessions.OAuth/src/VNLib.Plugins.Sessions.OAuth.csproj46
12 files changed, 1164 insertions, 0 deletions
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs
new file mode 100644
index 0000000..0e39c8a
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs
@@ -0,0 +1,197 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: AccessTokenEndpoint.cs
+*
+* AccessTokenEndpoint.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net;
+using System.Text.Json;
+
+using VNLib.Utils.Memory;
+using VNLib.Hashing.IdentityUtility;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Essentials.Oauth.Tokens;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Sql;
+using VNLib.Plugins.Extensions.Validation;
+
+namespace VNLib.Plugins.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
+ /// </summary>
+ internal sealed class AccessTokenEndpoint : ResourceEndpointBase
+ {
+ private readonly CreateTokenImpl CreateToken;
+ private readonly ApplicationStore Applications;
+
+ private readonly Task<ReadOnlyJsonWebKey?> JWTVerificationKey;
+
+ //override protection settings to allow most connections to authenticate
+ protected override ProtectionSettings EndpointProtectionSettings { get; } = new()
+ {
+ DisableBrowsersOnly = true,
+ DisableSessionsRequired = true,
+ DisableVerifySessionCors = true
+ };
+
+ public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore)
+ {
+ InitPathAndLog(path, pbase.Log);
+ CreateToken = tokenStore;
+ Applications = new(pbase.GetContextOptions(), pbase.GetPasswords());
+ //Try to get the application token key for verifying signed application JWTs
+ JWTVerificationKey = pbase.TryGetSecretAsync("application_token_key").ToJsonWebKey();
+ }
+
+
+ protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
+ {
+ //Check for refresh token
+ if (entity.RequestArgs.IsArgumentSet("grant_type", "refresh_token"))
+ {
+ //process a refresh token
+ }
+
+ //See if we have an application authorized with JWT
+ else if (entity.RequestArgs.IsArgumentSet("grant_type", "application"))
+ {
+ if(entity.RequestArgs.TryGetNonEmptyValue("token", out string? appJwt))
+ {
+ //Try to get and verify the app
+ UserApplication? app = GetApplicationFromJwt(appJwt);
+
+ //generate token
+ return await GenerateTokenAsync(entity, app);
+ }
+ }
+
+ //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))
+ {
+
+ 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
+ using PrivateString secretPv = new(secret, false);
+
+ //Get the application from apps store
+ UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv);
+
+ return await GenerateTokenAsync(entity, app);
+ }
+ }
+
+ entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type");
+ //Default to bad request
+ return VfReturnType.VirtualSkip;
+ }
+
+ private UserApplication? GetApplicationFromJwt(string jwtData)
+ {
+ ReadOnlyJsonWebKey? verificationKey = JWTVerificationKey.GetAwaiter().GetResult();
+
+ //Not enabled
+ if (verificationKey == null)
+ {
+ return null;
+ }
+
+ //Parse application token
+ using JsonWebToken jwt = JsonWebToken.Parse(jwtData);
+
+ //verify the application jwt
+ if (!jwt.VerifyFromJwk(verificationKey))
+ {
+ 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.TooManyRequests, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application");
+ return VfReturnType.VirtualSkip;
+ }
+
+ //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;
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs
new file mode 100644
index 0000000..6c9bff1
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs
@@ -0,0 +1,56 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: RevocationEndpoint.cs
+*
+* RevocationEndpoint.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Text.Json;
+
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Extensions.Loading;
+
+namespace VNLib.Plugins.Sessions.OAuth.Endpoints
+{
+ /// <summary>
+ /// An OAuth2 authorized endpoint for revoking the access token
+ /// held by the current connection
+ /// </summary>
+ [ConfigurationName("o2_revocation_endpoint")]
+ internal class RevocationEndpoint : O2EndpointBase
+ {
+
+ public RevocationEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config)
+ {
+ string? path = config["path"].GetString();
+ InitPathAndLog(path, pbase.Log);
+ }
+
+ protected override VfReturnType Post(HttpEntity entity)
+ {
+ //Revoke the access token, by invalidating it
+ entity.Session.Invalidate();
+ entity.CloseResponse(System.Net.HttpStatusCode.OK);
+ return VfReturnType.VirtualSkip;
+ }
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs
new file mode 100644
index 0000000..12a7ffe
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs
@@ -0,0 +1,74 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: IOauthSessionIdFactory.cs
+*
+* IOauthSessionIdFactory.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+using VNLib.Plugins.Sessions.Cache.Client;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ internal interface IOauthSessionIdFactory
+ {
+ /// <summary>
+ /// The maxium number of tokens allowed to be created per OAuth application
+ /// </summary>
+ int MaxTokensPerApp { get; }
+
+ /// <summary>
+ /// Allows for custom configuration of the newly created session and
+ /// the <see cref="IHttpEvent"/> its attached to
+ /// </summary>
+ /// <param name="session">The newly created session</param>
+ /// <param name="app">The application associated with the session</param>
+ /// <param name="entity">The http event that generated the new session</param>
+ void InitNewSession(RemoteSession session, UserApplication app, IHttpEvent entity);
+ /// <summary>
+ /// The time a session is valid for
+ /// </summary>
+ TimeSpan SessionValidFor { get; }
+
+ /// <summary>
+ /// Called when the session provider wishes to generate a new session
+ /// and required credential information to generate the new session
+ /// </summary>
+ /// <returns>The information genreated for the news ession</returns>
+ TokenAndSessionIdResult GenerateTokensAndId();
+
+ /// <summary>
+ /// The type of token this session provider generates
+ /// </summary>
+ string TokenType { get; }
+
+ /// <summary>
+ /// Attempts to recover a session id from
+ /// </summary>
+ /// <param name="entity">The entity to get the session-id for</param>
+ /// <param name="sessionId">The found ID for the session if accepted</param>
+ /// <returns>True if a session id was found or set for the session</returns>
+ bool TryGetSessionId(IHttpEvent entity, [NotNullWhen(true)] out string? sessionId);
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/O2AuthenticationPluginEntry.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/O2AuthenticationPluginEntry.cs
new file mode 100644
index 0000000..2edabd4
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/O2AuthenticationPluginEntry.cs
@@ -0,0 +1,58 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: O2AuthenticationPluginEntry.cs
+*
+* O2AuthenticationPluginEntry.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using VNLib.Utils.Logging;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ public sealed class O2AuthenticationPluginEntry : PluginBase
+ {
+ public override string PluginName => "Essentials.Oauth.Authentication";
+
+ private readonly O2SessionProviderEntry SessionProvider = new();
+
+ protected override void OnLoad()
+ {
+ try
+ {
+ //Load the session provider, that will only load the endpoints
+ SessionProvider.Load(this, Log);
+ }
+ catch(KeyNotFoundException kne)
+ {
+ Log.Error("Missing required configuration keys {err}", kne.Message);
+ }
+ }
+
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+ }
+
+ protected override void ProcessHostCommand(string cmd)
+ {
+ throw new NotImplementedException();
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs
new file mode 100644
index 0000000..073ed82
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs
@@ -0,0 +1,120 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: O2SessionProviderEntry.cs
+*
+* O2SessionProviderEntry.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json;
+
+using VNLib.Net.Http;
+using VNLib.Utils.Logging;
+using VNLib.Utils.Extensions;
+using VNLib.Data.Caching;
+using VNLib.Plugins.Sessions.Cache.Client;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Sessions;
+using VNLib.Plugins.Essentials.Oauth.Tokens;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+using VNLib.Plugins.Sessions.OAuth.Endpoints;
+using VNLib.Plugins.Extensions.VNCache;
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
+using VNLib.Plugins.Extensions.Loading.Sql;
+using VNLib.Plugins.Extensions.Loading.Events;
+
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+
+ public sealed class O2SessionProviderEntry : ISessionProvider
+ {
+ const string OAUTH2_CONFIG_KEY = "oauth2";
+
+ private OAuth2SessionProvider? _sessions;
+
+ public bool CanProcess(IHttpEvent entity)
+ {
+ //If authorization header is set try to process as oauth2 session
+ return _sessions != null && entity.Server.Headers.HeaderSet(System.Net.HttpRequestHeader.Authorization);
+ }
+
+ ValueTask<SessionHandle> ISessionProvider.GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
+ {
+ return _sessions!.GetSessionAsync(entity, cancellationToken);
+ }
+
+
+ public void Load(PluginBase plugin, ILogProvider localized)
+ {
+ IReadOnlyDictionary<string, JsonElement> oauth2Config = plugin.GetConfigForType<OAuth2SessionProvider>();
+
+ //Access token endpoint is optional
+ if (oauth2Config.TryGetValue("token_path", out JsonElement el))
+ {
+ //Init auth endpoint
+ AccessTokenEndpoint authEp = new(el.GetString()!, plugin, CreateTokenDelegateAsync);
+
+ //route auth endpoint
+ plugin.Route(authEp);
+ }
+
+ //Optional revocation endpoint
+ if (plugin.HasConfigForType<RevocationEndpoint>())
+ {
+ //Route revocation endpoint
+ plugin.Route<RevocationEndpoint>();
+ }
+
+ int cacheLimit = oauth2Config["cache_size"].GetInt32();
+ int maxTokensPerApp = oauth2Config["max_tokens_per_app"].GetInt32();
+ int sessionIdSize = (int)oauth2Config["access_token_size"].GetUInt32();
+ TimeSpan tokenValidFor = oauth2Config["token_valid_for_sec"].GetTimeSpan(TimeParseType.Seconds);
+ TimeSpan cleanupInterval = oauth2Config["gc_interval_sec"].GetTimeSpan(TimeParseType.Seconds);
+ string sessionIdPrefix = oauth2Config["cache_prefix"].GetString() ?? throw new KeyNotFoundException($"Missing required key 'cache_prefix' in '{OAUTH2_CONFIG_KEY}' config");
+
+ //init the id provider
+ OAuth2SessionIdProvider idProv = new(sessionIdPrefix, maxTokensPerApp, sessionIdSize, tokenValidFor);
+
+ //Get shared global-cache
+ IGlobalCacheProvider globalCache = plugin.GetGlobalCache(localized);
+
+ //Create cache store from global cache
+ GlobalCacheStore cacheStore = new(globalCache);
+
+ //Init session provider now that client is loaded
+ _sessions = new(cacheStore, cacheLimit, 100, idProv, plugin.GetContextOptions());
+
+ //Schedule cleanup interval with the plugin scheduler
+ plugin.ScheduleInterval(_sessions, cleanupInterval);
+
+ //Wait and cleanup expired sessions
+ _ = plugin.DeferTask(() => _sessions.CleanupExpiredSessionsAsync(localized, plugin.UnloadToken), 1000);
+
+ localized.Information("Session provider loaded");
+
+ }
+
+ private async Task<IOAuth2TokenResult?> CreateTokenDelegateAsync(HttpEntity entity, UserApplication app, CancellationToken cancellation)
+ {
+ return await _sessions!.CreateAccessTokenAsync(entity, app, cancellation).ConfigureAwait(false);
+ }
+ }
+} \ No newline at end of file
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2Session.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2Session.cs
new file mode 100644
index 0000000..0222737
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2Session.cs
@@ -0,0 +1,132 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: OAuth2Session.cs
+*
+* OAuth2Session.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+
+using VNLib.Net.Http;
+using VNLib.Plugins.Essentials.Sessions;
+using VNLib.Plugins.Sessions.Cache.Client;
+using VNLib.Plugins.Sessions.Cache.Client.Exceptions;
+
+using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ /// <summary>
+ /// The implementation of the OAuth2 session container for HTTP sessions
+ /// </summary>
+ internal sealed class OAuth2Session : RemoteSession
+ {
+ private readonly Action<OAuth2Session> InvalidateCache;
+
+ /// <summary>
+ /// Initalizes a new <see cref="OAuth2Session"/>
+ /// </summary>
+ /// <param name="sessionId">The session id (or token)</param>
+ /// <param name="client">The <see cref="IRemoteCacheStore"/> used as the backing cache provider</param>
+ /// <param name="backgroundTimeOut">The ammount of time to wait for a background operation (delete, update, get)</param>
+ /// <param name="invalidCache">Called when the session has been marked as invalid and the close even hook is being executed</param>
+ public OAuth2Session(string sessionId, IRemoteCacheStore client, TimeSpan backgroundTimeOut, Action<OAuth2Session> invalidCache)
+ : base(sessionId, client, backgroundTimeOut)
+ {
+ InvalidateCache = invalidCache;
+ IsInvalid = false;
+ }
+
+ public bool IsInvalid { get; private set; }
+
+
+ ///<inheritdoc/>
+ ///<exception cref="NotSupportedException"></exception>
+ public override string Token
+ {
+ get => throw new NotSupportedException("Token property is not supported for OAuth2 sessions");
+ set => throw new NotSupportedException("Token property is not supported for OAuth2 sessions");
+ }
+
+ ///<inheritdoc/>
+ protected override void IndexerSet(string key, string value)
+ {
+ //Guard protected entires
+ switch (key)
+ {
+ case TOKEN_ENTRY:
+ case LOGIN_TOKEN_ENTRY:
+ throw new InvalidOperationException("Token entry may not be changed!");
+ }
+ base.IndexerSet(key, value);
+ }
+ ///<inheritdoc/>
+ ///<exception cref="SessionStatusException"></exception>
+ public override async Task WaitAndLoadAsync(IHttpEvent entity, CancellationToken token = default)
+ {
+ //Wait to enter lock
+ await base.WaitAndLoadAsync(entity, token);
+ if (IsInvalid)
+ {
+ //Release lock
+ MainLock.Release();
+ throw new SessionStatusException("The session has been invalidated");
+ }
+ //Set session type
+ if (IsNew)
+ {
+ SessionType = SessionType.OAuth2;
+ }
+ }
+ ///<inheritdoc/>
+ protected override async ValueTask<Task?> UpdateResource(bool isAsync, IHttpEvent state)
+ {
+ Task? result = null;
+ //Invalid flag is set, so exit
+ if (IsInvalid)
+ {
+ result = Task.CompletedTask;
+ }
+ //Check flags in priority level, Invalid is highest state priority
+ else if (Flags.IsSet(INVALID_MSK))
+ {
+ //Clear all stored values
+ DataStore!.Clear();
+ //Delete the entity syncronously
+ await ProcessDeleteAsync();
+ //Set invalid flag
+ IsInvalid = true;
+ //Invlidate cache
+ InvalidateCache(this);
+ result = Task.CompletedTask;
+ }
+ else if (Flags.IsSet(MODIFIED_MSK))
+ {
+ //Send update to server
+ result = Task.Run(ProcessUpdateAsync);
+ }
+
+ //Clear all flags
+ Flags.ClearAll();
+
+ return result;
+ }
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs
new file mode 100644
index 0000000..5a30ec4
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs
@@ -0,0 +1,131 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: OAuth2SessionIdProvider.cs
+*
+* OAuth2SessionIdProvider.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Diagnostics.CodeAnalysis;
+
+using VNLib.Hashing;
+using VNLib.Net.Http;
+using VNLib.Utils.Memory;
+using VNLib.Utils.Extensions;
+using VNLib.Plugins.Essentials.Extensions;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+using VNLib.Plugins.Sessions.Cache.Client;
+using static VNLib.Plugins.Essentials.Oauth.OauthSessionExtensions;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ /// <summary>
+ /// Generates secure OAuth2 session tokens and initalizes new OAuth2 sessions
+ /// </summary>
+ internal class OAuth2SessionIdProvider : IOauthSessionIdFactory
+ {
+ private readonly string SessionIdPrefix;
+ private readonly int _bufferSize;
+ private readonly int _tokenSize;
+
+ ///<inheritdoc/>
+ public int MaxTokensPerApp { get; }
+ ///<inheritdoc/>
+ public TimeSpan SessionValidFor { get; }
+ ///<inheritdoc/>
+ string IOauthSessionIdFactory.TokenType => "Bearer";
+
+ public OAuth2SessionIdProvider(string sessionIdPrefix, int maxTokensPerApp, int tokenSize, TimeSpan validFor)
+ {
+ SessionIdPrefix = sessionIdPrefix;
+ MaxTokensPerApp = maxTokensPerApp;
+ SessionValidFor = validFor;
+ _tokenSize = tokenSize;
+ _bufferSize = tokenSize * 2;
+ }
+
+ ///<inheritdoc/>
+ bool IOauthSessionIdFactory.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.HasAuthorization(out string? token) && (token.Length + SessionIdPrefix.Length) <= _bufferSize)
+ {
+ //Compute session id from token
+ sessionId = ComputeSessionIdFromToken(token);
+
+ return true;
+ }
+ else
+ {
+ sessionId = null;
+ }
+
+ return false;
+ }
+
+ private string ComputeSessionIdFromToken(string token)
+ {
+ //Buffer to copy data to
+ using UnsafeMemoryHandle<char> buffer = Memory.UnsafeAlloc<char>(_bufferSize, true);
+
+ //Writer to accumulate data
+ ForwardOnlyWriter<char> writer = new(buffer.Span);
+
+ //Append session id prefix and token
+ writer.Append(SessionIdPrefix);
+ writer.Append(token);
+
+ //Compute base64 hash of token and
+ return ManagedHash.ComputeBase64Hash(writer.AsSpan(), HashAlg.SHA256);
+ }
+
+ ///<inheritdoc/>
+ TokenAndSessionIdResult IOauthSessionIdFactory.GenerateTokensAndId()
+ {
+ //Alloc buffer for random data
+ using UnsafeMemoryHandle<byte> mem = Memory.UnsafeAlloc<byte>(_tokenSize, true);
+
+ //Generate token from random cng bytes
+ RandomHash.GetRandomBytes(mem);
+
+ //Token is the raw value
+ string token = Convert.ToBase64String(mem.Span);
+
+ //The session id is the HMAC of the token
+ string sessionId = ComputeSessionIdFromToken(token);
+
+ //Clear buffer
+ Memory.InitializeBlock(mem.Span);
+
+ //Return sessid result
+ return new(sessionId, token, null);
+ }
+
+ ///<inheritdoc/>
+ void IOauthSessionIdFactory.InitNewSession(RemoteSession session, UserApplication app, IHttpEvent entity)
+ {
+ //Store session variables
+ session[APP_ID_ENTRY] = app.Id;
+ session[TOKEN_TYPE_ENTRY] = "client_credential,bearer";
+ session[SCOPES_ENTRY] = app.Permissions;
+ session.UserID = app.UserId;
+ }
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionProvider.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionProvider.cs
new file mode 100644
index 0000000..e10ae7d
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionProvider.cs
@@ -0,0 +1,230 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: OAuth2SessionProvider.cs
+*
+* OAuth2SessionProvider.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System;
+using System.Net;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Net.Http;
+using VNLib.Utils;
+using VNLib.Utils.Logging;
+using VNLib.Data.Caching.Exceptions;
+using VNLib.Plugins.Sessions.Cache.Client;
+using VNLib.Plugins.Essentials;
+using VNLib.Plugins.Essentials.Oauth;
+using VNLib.Plugins.Essentials.Sessions;
+using VNLib.Plugins.Essentials.Oauth.Tokens;
+using VNLib.Plugins.Essentials.Oauth.Applications;
+using VNLib.Plugins.Extensions.Loading.Events;
+using VNLib.Plugins.Extensions.Loading;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+
+ /// <summary>
+ /// Provides OAuth2 session management
+ /// </summary>
+ [ConfigurationName("oauth2")]
+ internal sealed class OAuth2SessionProvider : SessionCacheClient, ITokenManager, IIntervalScheduleable
+ {
+
+ private static readonly SessionHandle NotFoundHandle = new(null, FileProcessArgs.NotFound, null);
+
+ private static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10);
+
+
+ private readonly IOauthSessionIdFactory factory;
+ private readonly TokenStore TokenStore;
+ private readonly uint MaxConnections;
+
+ public OAuth2SessionProvider(IRemoteCacheStore client, int maxCacheItems, uint maxConnections, IOauthSessionIdFactory idFactory, DbContextOptions dbCtx)
+ : base(client, maxCacheItems)
+ {
+ factory = idFactory;
+ TokenStore = new(dbCtx);
+ MaxConnections = maxConnections;
+ }
+
+ ///<inheritdoc/>
+ protected override RemoteSession SessionCtor(string sessionId) => new OAuth2Session(sessionId, Store, BackgroundTimeout, InvalidatateCache);
+
+ private void InvalidatateCache(OAuth2Session session)
+ {
+ lock (CacheLock)
+ {
+ _ = CacheTable.Remove(session.SessionID);
+ }
+ }
+
+ ///<inheritdoc/>
+ public async ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken)
+ {
+ //Callback to close the session when the handle is closeed
+ static ValueTask HandleClosedAsync(ISession session, IHttpEvent entity)
+ {
+ return ((SessionBase)session).UpdateAndRelease(true, entity);
+ }
+ try
+ {
+ //Get session id
+ if (!factory.TryGetSessionId(entity, out string? sessionId))
+ {
+ //Id not allowed/found, so do not attach a session
+ return SessionHandle.Empty;
+ }
+
+ //Limit max number of waiting clients
+ if (WaitingConnections > MaxConnections)
+ {
+ //Set 503 for temporary unavail
+ entity.CloseResponse(HttpStatusCode.ServiceUnavailable);
+ return new SessionHandle(null, FileProcessArgs.VirtualSkip, null);
+ }
+
+ //Recover the session
+ RemoteSession session = await base.GetSessionAsync(entity, sessionId, cancellationToken);
+
+ //Session should not be new
+ if (session.IsNew)
+ {
+ //Invalidate the session, so it is deleted
+ session.Invalidate();
+ await session.UpdateAndRelease(true, entity);
+ return SessionHandle.Empty;
+ }
+ //Make sure session is still valid
+ if (session.Created.Add(factory.SessionValidFor) < DateTimeOffset.UtcNow)
+ {
+ //Invalidate the handle
+ session.Invalidate();
+ //Flush changes
+ await session.UpdateAndRelease(false, entity);
+ //Remove the token from the db backing store
+ await TokenStore.RevokeTokenAsync(sessionId, cancellationToken);
+ //close entity
+ entity.CloseResponseError(HttpStatusCode.Unauthorized, ErrorType.InvalidToken, "The token has expired");
+ //return a completed handle
+ return NotFoundHandle;
+ }
+
+ return new SessionHandle(session, HandleClosedAsync);
+ }
+ //Pass session exceptions
+ catch (SessionException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new SessionException("Exception raised while retreiving or loading OAuth2 session", ex);
+ }
+ }
+ ///<inheritdoc/>
+ public async Task<IOAuth2TokenResult?> CreateAccessTokenAsync(IHttpEvent 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)
+ {
+ return null;
+ }
+ //Create new session from the session id
+ RemoteSession session = SessionCtor(ids.SessionId);
+ await session.WaitAndLoadAsync(ev, cancellation);
+ try
+ {
+ //Init new session
+ factory.InitNewSession(session, app, ev);
+ }
+ finally
+ {
+ await session.UpdateAndRelease(false, ev);
+ }
+ //Init new token result to pass to client
+ return new OAuth2TokenResult()
+ {
+ ExpiresSeconds = (int)factory.SessionValidFor.TotalSeconds,
+ TokenType = factory.TokenType,
+ //Return token and refresh token
+ AccessToken = ids.AccessToken,
+ RefreshToken = ids.RefreshToken,
+ };
+ }
+ ///<inheritdoc/>
+ Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation)
+ {
+ return TokenStore.RevokeTokensAsync(tokens, cancellation);
+ }
+ ///<inheritdoc/>
+ Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation)
+ {
+ return TokenStore.RevokeTokenAsync(appId, cancellation);
+ }
+
+
+ /*
+ * Interval for removing expired tokens
+ */
+
+ ///<inheritdoc/>
+ async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken)
+ {
+ //Calculate valid token time
+ DateTime validAfter = DateTime.UtcNow.Subtract(factory.SessionValidFor);
+ //Remove tokens from db store
+ IReadOnlyCollection<ActiveToken> revoked = await TokenStore.CleanupExpiredTokensAsync(validAfter, cancellationToken);
+ //exception list
+ List<Exception>? errors = null;
+ //Remove all sessions from the store
+ foreach (ActiveToken token in revoked)
+ {
+ try
+ {
+ //Remove tokens by thier object id from cache
+ await base.Store.DeleteObjectAsync(token.Id, cancellationToken);
+ }
+ //Ignore if the object has already been removed
+ catch (ObjectNotFoundException)
+ {}
+ catch (Exception ex)
+ {
+ errors = new()
+ {
+ ex
+ };
+ }
+ }
+ if (errors?.Count > 0)
+ {
+ throw new AggregateException(errors);
+ }
+ if(revoked.Count > 0)
+ {
+ log.Debug("Cleaned up {0} expired tokens", revoked.Count);
+ }
+ }
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenResult.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenResult.cs
new file mode 100644
index 0000000..968825c
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenResult.cs
@@ -0,0 +1,37 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: OAuth2TokenResult.cs
+*
+* OAuth2TokenResult.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using VNLib.Plugins.Essentials.Oauth.Tokens;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ internal class OAuth2TokenResult : IOAuth2TokenResult
+ {
+ public string? IdentityToken { get; }
+ public string? AccessToken { get; set; }
+ public string? RefreshToken { get; set; }
+ public string? TokenType { get; set; }
+ public int ExpiresSeconds { get; set; }
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OauthTokenResponseMessage.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OauthTokenResponseMessage.cs
new file mode 100644
index 0000000..2a063b0
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OauthTokenResponseMessage.cs
@@ -0,0 +1,42 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: OauthTokenResponseMessage.cs
+*
+* OauthTokenResponseMessage.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+using System.Text.Json.Serialization;
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ public sealed class OauthTokenResponseMessage
+ {
+ [JsonPropertyName("access_token")]
+ public string? AccessToken { get; set; }
+ [JsonPropertyName("refresh_token")]
+ public string? RefreshToken { get; set; }
+ [JsonPropertyName("token_type")]
+ public string? TokenType { get; set; }
+ [JsonPropertyName("expires_in")]
+ public int Expires { get; set; }
+ [JsonPropertyName("id_token")]
+ public string? IdToken { get; set; }
+ }
+} \ No newline at end of file
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs
new file mode 100644
index 0000000..6ae2af4
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs
@@ -0,0 +1,41 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Sessions.OAuth
+* File: TokenAndSessionIdResult.cs
+*
+* TokenAndSessionIdResult.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify
+* it under the terms of the GNU Affero General Public License as
+* published by the Free Software Foundation, either version 3 of the
+* License, or (at your option) any later version.
+*
+* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful,
+* but WITHOUT ANY WARRANTY; without even the implied warranty of
+* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+* GNU Affero General Public License for more details.
+*
+* You should have received a copy of the GNU Affero General Public License
+* along with this program. If not, see https://www.gnu.org/licenses/.
+*/
+
+
+namespace VNLib.Plugins.Sessions.OAuth
+{
+ public readonly struct TokenAndSessionIdResult
+ {
+ public readonly string SessionId;
+ public readonly string AccessToken;
+ public readonly string? RefreshToken;
+
+ public TokenAndSessionIdResult(string sessionId, string token, string? refreshToken)
+ {
+ SessionId = sessionId;
+ AccessToken = token;
+ RefreshToken = refreshToken;
+ }
+ }
+}
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/VNLib.Plugins.Sessions.OAuth.csproj b/libs/VNLib.Plugins.Sessions.OAuth/src/VNLib.Plugins.Sessions.OAuth.csproj
new file mode 100644
index 0000000..768a802
--- /dev/null
+++ b/libs/VNLib.Plugins.Sessions.OAuth/src/VNLib.Plugins.Sessions.OAuth.csproj
@@ -0,0 +1,46 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <GenerateDocumentationFile>True</GenerateDocumentationFile>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2023 Vaughn Nugent</Copyright>
+ <AssemblyName>VNLib.Plugins.Sessions.OAuth</AssemblyName>
+ <RootNamespace>VNLib.Plugins.Sessions.OAuth</RootNamespace>
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
+ <Version>1.0.1.1</Version>
+ <PackageProjectUrl>https://www.vaughnugent.com/resources/software</PackageProjectUrl>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.PluginBase\src\VNLib.Plugins.PluginBase.csproj" />
+ <ProjectReference Include="..\..\..\..\DataCaching\lib\VNLib.Plugins.Extensions.VNCache\src\VNLib.Plugins.Extensions.VNCache.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj" />
+ <ProjectReference Include="..\..\..\..\Oauth\Libs\VNLib.Plugins.Essentials.Oauth\src\VNLib.Plugins.Essentials.Oauth.csproj" />
+ <ProjectReference Include="..\..\VNLib.Plugins.Sessions.Cache.Client\src\VNLib.Plugins.Sessions.Cache.Client.csproj" />
+ </ItemGroup>
+
+ <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\VNLib\devplugins\RuntimeAssets\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+ <Target Name="PreBuild" BeforeTargets="PreBuildEvent">
+ <Exec Command="erase &quot;F:\Programming\VNLib\devplugins\RuntimeAssets\$(TargetName)&quot; /q &gt; nul" />
+ </Target>
+
+</Project>