diff options
author | vnugent <public@vaughnnugent.com> | 2023-03-09 01:48:40 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-03-09 01:48:40 -0500 |
commit | d673bd34945699df96e38c54f70352608430fbc4 (patch) | |
tree | dd5e17d02f3fe73e4d1a54689bd9c7d41f1a5a71 /libs/VNLib.Plugins.Sessions.OAuth/src | |
parent | 11a8cea8a6445bd5127eb4c97fc582cd944f72ea (diff) |
Omega cache, session, and account provider complete overhaul
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.OAuth/src')
13 files changed, 507 insertions, 413 deletions
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs index f01b764..9f0f35d 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth @@ -25,7 +25,6 @@ using System; using System.Net; using System.Text.Json; -using System.Threading; using System.Threading.Tasks; using VNLib.Utils.Memory; @@ -42,17 +41,16 @@ 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> + [ConfigurationName(O2SessionProviderEntry.OAUTH2_CONFIG_KEY)] internal sealed class AccessTokenEndpoint : ResourceEndpointBase { - private readonly CreateTokenImpl CreateToken; + private readonly IApplicationTokenFactory TokenFactory; private readonly ApplicationStore Applications; - private readonly Task<ReadOnlyJsonWebKey?> JWTVerificationKey; //override protection settings to allow most connections to authenticate @@ -63,11 +61,17 @@ namespace VNLib.Plugins.Sessions.OAuth.Endpoints DisableVerifySessionCors = true }; - public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore) + public AccessTokenEndpoint(PluginBase pbase, IConfigScope config) { + string? path = config["token_path"].GetString();; + InitPathAndLog(path, pbase.Log); - CreateToken = tokenStore; + + //Get the session provider, as its a token factory + TokenFactory = pbase.GetOrCreateSingleton<OAuth2SessionProvider>(); + 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(); } @@ -172,7 +176,7 @@ namespace VNLib.Plugins.Sessions.OAuth.Endpoints return VfReturnType.VirtualSkip; } - IOAuth2TokenResult? result = await CreateToken(entity, app, entity.EventCancellation); + IOAuth2TokenResult? result = await TokenFactory.CreateAccessTokenAsync(entity, app, entity.EventCancellation); if (result == null) { diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs index 81f82c2..45a8391 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/RevocationEndpoint.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth @@ -22,10 +22,6 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; -using System.Text.Json; -using System.Collections.Generic; - using VNLib.Plugins.Essentials; using VNLib.Plugins.Essentials.Oauth; using VNLib.Plugins.Extensions.Loading; @@ -40,7 +36,7 @@ namespace VNLib.Plugins.Sessions.OAuth.Endpoints internal class RevocationEndpoint : O2EndpointBase { - public RevocationEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) + public RevocationEndpoint(PluginBase pbase, IConfigScope config) { string? path = config["path"].GetString(); InitPathAndLog(path, pbase.Log); diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/GetTokenResult.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/GetTokenResult.cs new file mode 100644 index 0000000..0587931 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/GetTokenResult.cs @@ -0,0 +1,36 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: GetTokenResult.cs +* +* GetTokenResult.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 +{ + + /// <summary> + /// The result of generating a new access token + /// </summary> + /// <param name="AccessToken">The new access token</param> + /// <param name="RefreshToken">The optional refresh token</param> + public readonly record struct GetTokenResult(string AccessToken, string? RefreshToken) + { } +} diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/IApplicationTokenFactory.cs index 6ae2af4..7f65ec3 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/IApplicationTokenFactory.cs @@ -1,11 +1,11 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth -* File: TokenAndSessionIdResult.cs +* File: IApplicationTokenFactory.cs * -* TokenAndSessionIdResult.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger +* IApplicationTokenFactory.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 @@ -22,20 +22,17 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ +using System.Threading; +using System.Threading.Tasks; + +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Oauth.Tokens; +using VNLib.Plugins.Essentials.Oauth.Applications; namespace VNLib.Plugins.Sessions.OAuth { - public readonly struct TokenAndSessionIdResult + interface IApplicationTokenFactory { - 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; - } + Task<IOAuth2TokenResult?> CreateAccessTokenAsync(IHttpEvent ev, UserApplication app, CancellationToken cancellation); } -} +}
\ No newline at end of file diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs index 12a7ffe..3fe508f 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/IOauthSessionIdFactory.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth @@ -23,11 +23,6 @@ */ 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 { @@ -37,15 +32,7 @@ namespace VNLib.Plugins.Sessions.OAuth /// 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> @@ -56,19 +43,11 @@ namespace VNLib.Plugins.Sessions.OAuth /// and required credential information to generate the new session /// </summary> /// <returns>The information genreated for the news ession</returns> - TokenAndSessionIdResult GenerateTokensAndId(); + GetTokenResult 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/O2SessionProviderEntry.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs index 92ea020..0437541 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/O2SessionProviderEntry.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth @@ -23,34 +23,22 @@ */ using System; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using System.Collections.Generic; 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.Essentials.Sessions; 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"; + public const string OAUTH2_CONFIG_KEY = "oauth2"; private OAuth2SessionProvider? _sessions; @@ -63,21 +51,17 @@ namespace VNLib.Plugins.Sessions.OAuth 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>(); + IConfigScope o2Config = plugin.GetConfig(OAUTH2_CONFIG_KEY); //Access token endpoint is optional - if (oauth2Config.TryGetValue("token_path", out JsonElement el)) + if (o2Config.ContainsKey("token_path")) { - //Init auth endpoint - AccessTokenEndpoint authEp = new(el.GetString()!, plugin, CreateTokenDelegateAsync); - - //route auth endpoint - plugin.Route(authEp); + //Create token endpoint + plugin.Route<AccessTokenEndpoint>(); } //Optional revocation endpoint @@ -87,38 +71,11 @@ namespace VNLib.Plugins.Sessions.OAuth 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.ObserveTask(() => _sessions.CleanupExpiredSessionsAsync(localized, plugin.UnloadToken), 1000); + //Init session provider + _sessions = plugin.GetOrCreateSingleton<OAuth2SessionProvider>(); + _sessions.SetLog(localized); 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 index 916f55c..605ccbd 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2Session.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2Session.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth @@ -23,42 +23,34 @@ */ using System; -using System.Threading; -using System.Threading.Tasks; +using System.Collections.Generic; using VNLib.Net.Http; using VNLib.Plugins.Essentials.Sessions; +using VNLib.Plugins.Essentials.Extensions; 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; + public OAuth2Session(string sessionId, IDictionary<string, string> data, bool isNew) + : base(sessionId, data, isNew) + {} - /// <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) + public void InitNewSession(IHttpEvent entity) { - InvalidateCache = invalidCache; - IsInvalid = false; + SessionType = SessionType.Web; + Created = DateTimeOffset.UtcNow; + //Set user-ip address + UserIP = entity.Server.GetTrustedIp(); } - public bool IsInvalid { get; private set; } - - ///<inheritdoc/> ///<exception cref="NotSupportedException"></exception> public override string Token @@ -79,56 +71,5 @@ namespace VNLib.Plugins.Sessions.OAuth } 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/OAuth2SessionConfig.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionConfig.cs new file mode 100644 index 0000000..5eb9c0c --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionConfig.cs @@ -0,0 +1,70 @@ +/* +* Copyright (c) 2023 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; +using System.Text.Json.Serialization; + +using VNLib.Plugins.Extensions.Loading; + + +namespace VNLib.Plugins.Sessions.OAuth +{ + sealed class OAuth2SessionConfig : IOnConfigValidation + { + [JsonPropertyName("max_tokens_per_app")] + public int MaxTokensPerApp { get; set; } = 10; + + [JsonPropertyName("access_token_size")] + public int AccessTokenSize { get; set; } = 64; + + [JsonPropertyName("token_valid_for_sec")] + public int TokenLifeTimeSeconds { get; set; } = 3600; + + [JsonPropertyName("cache_prefix")] + public string CachePrefix { get; set; } = "oauth2"; + + public void Validate() + { + if (MaxTokensPerApp < 1) + { + throw new ArgumentOutOfRangeException("max_tokens_per_app", "You must configure at least 1 Oatuh2 access token per application, or disable this plugin"); + } + + if (AccessTokenSize < 16) + { + throw new ArgumentOutOfRangeException("access_token_size", "You must configure an access token size of at least 16 bytes in length"); + } + + if (TokenLifeTimeSeconds < 1) + { + throw new ArgumentOutOfRangeException("token_valid_for_sec", "You must configure an access token lifetime"); + } + + if (string.IsNullOrWhiteSpace(CachePrefix)) + { + throw new ArgumentException("You must specify a cache prefix", "cache_prefix"); + } + } + } +}
\ No newline at end of file diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionFactory.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionFactory.cs new file mode 100644 index 0000000..d811e25 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionFactory.cs @@ -0,0 +1,41 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: OAuth2SessionFactory.cs +* +* OAuth2SessionFactory.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.Collections.Generic; + +using VNLib.Net.Http; +using VNLib.Plugins.Sessions.Cache.Client; + +namespace VNLib.Plugins.Sessions.OAuth +{ + internal sealed class OAuth2SessionFactory : ISessionFactory<OAuth2Session> + { + ///<inheritdoc/> + public OAuth2Session? GetNewSession(IHttpEvent entity, string sessionId, IDictionary<string, string>? sessionData) + { + //Initial data should not be null, if so, do not attach session + return sessionData == null ? null : new(sessionId, sessionData, false); + } + } +}
\ No newline at end of file diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs deleted file mode 100644 index 5e8ad6b..0000000 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs +++ /dev/null @@ -1,131 +0,0 @@ -/* -* 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 = MemoryUtil.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 = MemoryUtil.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 - MemoryUtil.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 index 7047e6e..1797ddb 100644 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionProvider.cs +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionProvider.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Sessions.OAuth @@ -28,20 +28,18 @@ using System.Threading; using System.Threading.Tasks; using System.Collections.Generic; -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; +using VNLib.Plugins.Extensions.Loading.Sql; using VNLib.Plugins.Extensions.Loading.Events; +using static VNLib.Plugins.Essentials.Oauth.OauthSessionExtensions; namespace VNLib.Plugins.Sessions.OAuth { @@ -49,139 +47,145 @@ 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); + [ConfigurationName(O2SessionProviderEntry.OAUTH2_CONFIG_KEY)] + internal sealed class OAuth2SessionProvider : ISessionProvider, ITokenManager, IApplicationTokenFactory + { + private static readonly SessionHandle Skip = new(null, FileProcessArgs.VirtualSkip, null); - - private readonly IOauthSessionIdFactory factory; + private readonly OAuth2SessionStore _sessions; + private readonly IOauthSessionIdFactory _tokenFactory; private readonly TokenStore TokenStore; - private readonly uint MaxConnections; - - public OAuth2SessionProvider(IRemoteCacheStore client, int maxCacheItems, uint maxConnections, IOauthSessionIdFactory idFactory, DbContextOptions dbCtx) - : base(client, maxCacheItems) + private readonly string _tokenTypeString; + private readonly uint _maxConnections; + + private uint _waitingConnections; + + public bool IsConnected => _sessions.IsConnected; + + public OAuth2SessionProvider(PluginBase plugin, IConfigScope config) { - factory = idFactory; - TokenStore = new(dbCtx); - MaxConnections = maxConnections; + _sessions = plugin.GetOrCreateSingleton<OAuth2SessionStore>(); + _tokenFactory = plugin.GetOrCreateSingleton<OAuth2TokenFactory>(); + TokenStore = new(plugin.GetContextOptions()); + _tokenTypeString = $"client_credential,{_tokenFactory.TokenType}"; } - ///<inheritdoc/> - protected override RemoteSession SessionCtor(string sessionId) => new OAuth2Session(sessionId, Store, BackgroundTimeout, InvalidatateCache); + public void SetLog(ILogProvider log) => _sessions.SetLog(log); - private void InvalidatateCache(OAuth2Session session) + public ValueTask<SessionHandle> GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) { - lock (CacheLock) + //Limit max number of waiting clients and make sure were connected + if (!_sessions.IsConnected || _waitingConnections > _maxConnections) { - _ = CacheTable.Remove(session.SessionID); + //Set 503 for temporary unavail + entity.CloseResponse(HttpStatusCode.ServiceUnavailable); + return ValueTask.FromResult(Skip); } - } - - ///<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) + ValueTask<OAuth2Session?> result = _sessions.GetSessionAsync(entity, cancellationToken); + + if (result.IsCompleted) { - return ((SessionBase)session).UpdateAndRelease(true, entity); + OAuth2Session? session = result.GetAwaiter().GetResult(); + + //Post process and get handle for session + SessionHandle handle = PostProcess(session); + + return ValueTask.FromResult(handle); } - try + else { - //Get session id - if (!factory.TryGetSessionId(entity, out string? sessionId)) - { - //Id not allowed/found, so do not attach a session - return SessionHandle.Empty; - } + return new(AwaitAsyncGet(result)); + } + } - //Limit max number of waiting clients - if (!IsConnected || WaitingConnections > MaxConnections) - { - //Set 503 for temporary unavail - entity.CloseResponse(HttpStatusCode.ServiceUnavailable); - return new SessionHandle(null, FileProcessArgs.VirtualSkip, null); - } + private async Task<SessionHandle> AwaitAsyncGet(ValueTask<OAuth2Session?> async) + { + //Inct wait count while async waiting + _waitingConnections++; + try + { + //await the session + OAuth2Session? session = await async.ConfigureAwait(false); - //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); + //return empty session handle if the session could not be found + return PostProcess(session); } - //Pass session exceptions - catch (SessionException) + finally { - throw; + _waitingConnections--; } - catch (Exception ex) + } + + private SessionHandle PostProcess(OAuth2Session? session) + { + if (session == null) { - throw new SessionException("Exception raised while retreiving or loading OAuth2 session", ex); + return SessionHandle.Empty; + } + + //Make sure the session has not expired yet + if (session.Created.Add(_tokenFactory.SessionValidFor) < DateTimeOffset.UtcNow) + { + //Invalidate the session, so its technically valid for this request, but will be cleared on this handle close cycle + session.Invalidate(); + + //Clears important security variables + InitNewSession(session, null); } + + return new SessionHandle(session, OnSessionReleases); } + + private ValueTask OnSessionReleases(ISession session, IHttpEvent entity) => _sessions.ReleaseSessionAsync((OAuth2Session)session, entity); + ///<inheritdoc/> public async Task<IOAuth2TokenResult?> CreateAccessTokenAsync(IHttpEvent ev, UserApplication app, CancellationToken cancellation) { //Get a new session for the current connection - TokenAndSessionIdResult ids = factory.GenerateTokensAndId(); + GetTokenResult ids = _tokenFactory.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.AccessToken, app.Id!, ids.RefreshToken, _tokenFactory.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); - } + + //Create new session + OAuth2Session newSession = _sessions.CreateSession(ev, ids.AccessToken); + + //Init the new session with application information + InitNewSession(newSession, app); + + //Commit the new session + await _sessions.CommitSessionAsync(newSession); + //Init new token result to pass to client return new OAuth2TokenResult() { - ExpiresSeconds = (int)factory.SessionValidFor.TotalSeconds, - TokenType = factory.TokenType, + ExpiresSeconds = (int)_tokenFactory.SessionValidFor.TotalSeconds, + TokenType = _tokenFactory.TokenType, //Return token and refresh token AccessToken = ids.AccessToken, RefreshToken = ids.RefreshToken, }; } + + private void InitNewSession(OAuth2Session session, UserApplication? app) + { + //Store session variables + session[APP_ID_ENTRY] = app?.Id; + session[TOKEN_TYPE_ENTRY] = _tokenTypeString; + session[SCOPES_ENTRY] = app?.Permissions; + session.UserID = app?.UserId; + } + ///<inheritdoc/> Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation) { return TokenStore.RevokeTokensAsync(tokens, cancellation); } + ///<inheritdoc/> Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation) { @@ -193,11 +197,11 @@ namespace VNLib.Plugins.Sessions.OAuth * Interval for removing expired tokens */ - ///<inheritdoc/> - async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) + [AsyncInterval(Minutes = 2)] + private async Task OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) { //Calculate valid token time - DateTime validAfter = DateTime.UtcNow.Subtract(factory.SessionValidFor); + DateTime validAfter = DateTime.UtcNow.Subtract(_tokenFactory.SessionValidFor); //Remove tokens from db store IReadOnlyCollection<ActiveToken> revoked = await TokenStore.CleanupExpiredTokensAsync(validAfter, cancellationToken); //exception list @@ -208,17 +212,17 @@ namespace VNLib.Plugins.Sessions.OAuth try { //Remove tokens by thier object id from cache - await base.Store.DeleteObjectAsync(token.Id, cancellationToken); + await _sessions.DeleteTokenAsync(token.Id, cancellationToken); } //Ignore if the object has already been removed catch (ObjectNotFoundException) {} catch (Exception ex) { - errors = new() - { - ex - }; +#pragma warning disable CA1508 // Avoid dead conditional code + errors ??= new(); +#pragma warning restore CA1508 // Avoid dead conditional code + errors.Add(ex); } } if (errors?.Count > 0) diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionStore.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionStore.cs new file mode 100644 index 0000000..8719002 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionStore.cs @@ -0,0 +1,101 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: OAuth2SessionStore.cs +* +* OAuth2SessionStore.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.Threading; +using System.Threading.Tasks; +using System.Collections.Generic; + +using VNLib.Hashing; +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Data.Caching; +using VNLib.Plugins.Sessions.Cache.Client; +using VNLib.Plugins.Extensions.VNCache; +using VNLib.Plugins.Extensions.Loading; + + +namespace VNLib.Plugins.Sessions.OAuth +{ + [ConfigurationName(O2SessionProviderEntry.OAUTH2_CONFIG_KEY)] + internal sealed class OAuth2SessionStore : SessionStore<OAuth2Session> + { + private ILogProvider _log; + + protected override ISessionIdFactory IdFactory { get; } + protected override IRemoteCacheStore Cache { get; } + protected override ISessionFactory<OAuth2Session> SessionFactory { get; } + protected override ILogProvider Log => _log; + + public bool IsConnected => Cache.IsConnected; + + public OAuth2SessionStore(PluginBase plugin, IConfigScope config) + { + OAuth2SessionConfig o2Conf = config.DeserialzeAndValidate<OAuth2SessionConfig>(); + + //Get global cache + IGlobalCacheProvider cache = plugin.GetOrCreateSingleton<VnGlobalCache>() + .GetPrefixedCache(o2Conf.CachePrefix, HashAlg.SHA256); + + //Create remote cache + Cache = new GlobalCacheStore(cache); + + IdFactory = plugin.GetOrCreateSingleton<OAuth2TokenFactory>(); + + SessionFactory = new OAuth2SessionFactory(); + + //Default to plugin cache + _log = plugin.Log; + } + + public void SetLog(ILogProvider log) + { + _log = log; + } + + + public Task DeleteTokenAsync(string token, CancellationToken cancellation) + { + return Cache.DeleteObjectAsync(token, cancellation); + } + + public OAuth2Session CreateSession(IHttpEvent entity, string sessionId) + { + //Get the new session + OAuth2Session session = SessionFactory.GetNewSession(entity, sessionId, new Dictionary<string, string>(10)); + //Configure the new session for use + session.InitNewSession(entity); + + return session; + } + + public async Task CommitSessionAsync(OAuth2Session session) + { + IDictionary<string, string> sessionData = session.GetSessionData(); + //Write data to cache + await Cache.AddOrUpdateObjectAsync(session.SessionID, null, sessionData); + //Good programming, update session + session.SessionUpdateComplete(); + } + } +}
\ No newline at end of file diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenFactory.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenFactory.cs new file mode 100644 index 0000000..b452e29 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenFactory.cs @@ -0,0 +1,99 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: OAuth2TokenFactory.cs +* +* OAuth2TokenFactory.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.Hashing; +using VNLib.Net.Http; +using VNLib.Plugins.Sessions.Cache.Client; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Essentials.Extensions; + + +namespace VNLib.Plugins.Sessions.OAuth +{ + [ConfigurationName(O2SessionProviderEntry.OAUTH2_CONFIG_KEY)] + internal sealed class OAuth2TokenFactory : ISessionIdFactory, IOauthSessionIdFactory + { + private readonly OAuth2SessionConfig _config; + + public OAuth2TokenFactory(PluginBase plugin, IConfigScope config) + { + //Get the oauth2 config + _config = config.DeserialzeAndValidate<OAuth2SessionConfig>(); + } + + /* + * ID Regeneration is always false as OAuth2 sessions + * do not allow dynamic ID updates, they require a + * negotiation + */ + + bool ISessionIdFactory.RegenerationSupported => false; + + /* + * Connections that do not identify themselves, via a token are + * not valid. ID/Tokens must be created at once during + * authentication stage. + */ + + bool ISessionIdFactory.RegenIdOnEmptyEntry => false; + + + ///<inheritdoc/> + int IOauthSessionIdFactory.MaxTokensPerApp => _config.MaxTokensPerApp; + + ///<inheritdoc/> + TimeSpan IOauthSessionIdFactory.SessionValidFor => TimeSpan.FromSeconds(_config.TokenLifeTimeSeconds); + + ///<inheritdoc/> + string IOauthSessionIdFactory.TokenType => "Bearer"; + + ///<inheritdoc/> + bool ISessionIdFactory.CanService(IHttpEvent entity) + { + return entity.Server.HasAuthorization(out _); + } + + ///<inheritdoc/> + public GetTokenResult GenerateTokensAndId() + { + //Token is the raw value + string token = RandomHash.GetRandomBase64(_config.AccessTokenSize); + + //Return sessid result + return new(token, null); + } + + string ISessionIdFactory.RegenerateId(IHttpEvent entity) + { + throw new NotSupportedException("Id regeneration is not supported for OAuth2 sessions"); + } + + string? ISessionIdFactory.TryGetSessionId(IHttpEvent entity) + { + return entity.Server.HasAuthorization(out string? token) ? token : null; + } + } +}
\ No newline at end of file |