From d673bd34945699df96e38c54f70352608430fbc4 Mon Sep 17 00:00:00 2001 From: vnugent Date: Thu, 9 Mar 2023 01:48:40 -0500 Subject: Omega cache, session, and account provider complete overhaul --- .../src/Endpoints/AccessTokenEndpoint.cs | 20 +- .../src/Endpoints/RevocationEndpoint.cs | 8 +- .../src/GetTokenResult.cs | 36 ++++ .../src/IApplicationTokenFactory.cs | 38 ++++ .../src/IOauthSessionIdFactory.cs | 27 +-- .../src/O2SessionProviderEntry.cs | 65 ++----- .../src/OAuth2Session.cs | 83 ++------ .../src/OAuth2SessionConfig.cs | 70 +++++++ .../src/OAuth2SessionFactory.cs | 41 ++++ .../src/OAuth2SessionIdProvider.cs | 131 ------------- .../src/OAuth2SessionProvider.cs | 212 +++++++++++---------- .../src/OAuth2SessionStore.cs | 101 ++++++++++ .../src/OAuth2TokenFactory.cs | 99 ++++++++++ .../src/TokenAndSessionIdResult.cs | 41 ---- 14 files changed, 533 insertions(+), 439 deletions(-) create mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/GetTokenResult.cs create mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/IApplicationTokenFactory.cs create mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionConfig.cs create mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionFactory.cs delete mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionIdProvider.cs create mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2SessionStore.cs create mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/OAuth2TokenFactory.cs delete mode 100644 libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs (limited to 'libs/VNLib.Plugins.Sessions.OAuth/src') 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 CreateTokenImpl(HttpEntity ev, UserApplication application, CancellationToken cancellation = default); /// /// Grants authorization to OAuth2 clients to protected resources /// with access tokens /// + [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 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(); + 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 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 +{ + + /// + /// The result of generating a new access token + /// + /// The new access token + /// The optional refresh token + public readonly record struct GetTokenResult(string AccessToken, string? RefreshToken) + { } +} diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/IApplicationTokenFactory.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/IApplicationTokenFactory.cs new file mode 100644 index 0000000..7f65ec3 --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/IApplicationTokenFactory.cs @@ -0,0 +1,38 @@ +/* +* Copyright (c) 2023 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: IApplicationTokenFactory.cs +* +* 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 +* 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 VNLib.Net.Http; +using VNLib.Plugins.Essentials.Oauth.Tokens; +using VNLib.Plugins.Essentials.Oauth.Applications; + +namespace VNLib.Plugins.Sessions.OAuth +{ + interface IApplicationTokenFactory + { + Task 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 /// int MaxTokensPerApp { get; } - - /// - /// Allows for custom configuration of the newly created session and - /// the its attached to - /// - /// The newly created session - /// The application associated with the session - /// The http event that generated the new session - void InitNewSession(RemoteSession session, UserApplication app, IHttpEvent entity); + /// /// The time a session is valid for /// @@ -56,19 +43,11 @@ namespace VNLib.Plugins.Sessions.OAuth /// and required credential information to generate the new session /// /// The information genreated for the news ession - TokenAndSessionIdResult GenerateTokensAndId(); + GetTokenResult GenerateTokensAndId(); /// /// The type of token this session provider generates /// string TokenType { get; } - - /// - /// Attempts to recover a session id from - /// - /// The entity to get the session-id for - /// The found ID for the session if accepted - /// True if a session id was found or set for the session - 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 ISessionProvider.GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) { return _sessions!.GetSessionAsync(entity, cancellationToken); - } - + } public void Load(PluginBase plugin, ILogProvider localized) { - IReadOnlyDictionary oauth2Config = plugin.GetConfigForType(); + 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(); } //Optional revocation endpoint @@ -87,38 +71,11 @@ namespace VNLib.Plugins.Sessions.OAuth plugin.Route(); } - 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(); + _sessions.SetLog(localized); localized.Information("Session provider loaded"); - - } - - private async Task 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 { + /// /// The implementation of the OAuth2 session container for HTTP sessions /// internal sealed class OAuth2Session : RemoteSession { - private readonly Action InvalidateCache; + public OAuth2Session(string sessionId, IDictionary data, bool isNew) + : base(sessionId, data, isNew) + {} - /// - /// Initalizes a new - /// - /// The session id (or token) - /// The used as the backing cache provider - /// The ammount of time to wait for a background operation (delete, update, get) - /// Called when the session has been marked as invalid and the close even hook is being executed - public OAuth2Session(string sessionId, IRemoteCacheStore client, TimeSpan backgroundTimeOut, Action 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; } - - /// /// public override string Token @@ -79,56 +71,5 @@ namespace VNLib.Plugins.Sessions.OAuth } base.IndexerSet(key, value); } - /// - /// - 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; - } - } - /// - protected override async ValueTask 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 + { + /// + public OAuth2Session? GetNewSession(IHttpEvent entity, string sessionId, IDictionary? 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 -{ - /// - /// Generates secure OAuth2 session tokens and initalizes new OAuth2 sessions - /// - internal class OAuth2SessionIdProvider : IOauthSessionIdFactory - { - private readonly string SessionIdPrefix; - private readonly int _bufferSize; - private readonly int _tokenSize; - - /// - public int MaxTokensPerApp { get; } - /// - public TimeSpan SessionValidFor { get; } - /// - 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; - } - - /// - 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 buffer = MemoryUtil.UnsafeAlloc(_bufferSize, true); - - //Writer to accumulate data - ForwardOnlyWriter 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); - } - - /// - TokenAndSessionIdResult IOauthSessionIdFactory.GenerateTokensAndId() - { - //Alloc buffer for random data - using UnsafeMemoryHandle mem = MemoryUtil.UnsafeAlloc(_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); - } - - /// - 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 /// /// Provides OAuth2 session management /// - [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(); + _tokenFactory = plugin.GetOrCreateSingleton(); + TokenStore = new(plugin.GetContextOptions()); + _tokenTypeString = $"client_credential,{_tokenFactory.TokenType}"; } - /// - 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 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); } - } - - /// - public async ValueTask GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) - { - //Callback to close the session when the handle is closeed - static ValueTask HandleClosedAsync(ISession session, IHttpEvent entity) + ValueTask 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 AwaitAsyncGet(ValueTask 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); + /// public async Task 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; + } + /// Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection tokens, CancellationToken cancellation) { return TokenStore.RevokeTokensAsync(tokens, cancellation); } + /// Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation) { @@ -193,11 +197,11 @@ namespace VNLib.Plugins.Sessions.OAuth * Interval for removing expired tokens */ - /// - 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 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 + { + private ILogProvider _log; + + protected override ISessionIdFactory IdFactory { get; } + protected override IRemoteCacheStore Cache { get; } + protected override ISessionFactory SessionFactory { get; } + protected override ILogProvider Log => _log; + + public bool IsConnected => Cache.IsConnected; + + public OAuth2SessionStore(PluginBase plugin, IConfigScope config) + { + OAuth2SessionConfig o2Conf = config.DeserialzeAndValidate(); + + //Get global cache + IGlobalCacheProvider cache = plugin.GetOrCreateSingleton() + .GetPrefixedCache(o2Conf.CachePrefix, HashAlg.SHA256); + + //Create remote cache + Cache = new GlobalCacheStore(cache); + + IdFactory = plugin.GetOrCreateSingleton(); + + 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(10)); + //Configure the new session for use + session.InitNewSession(entity); + + return session; + } + + public async Task CommitSessionAsync(OAuth2Session session) + { + IDictionary 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(); + } + + /* + * 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; + + + /// + int IOauthSessionIdFactory.MaxTokensPerApp => _config.MaxTokensPerApp; + + /// + TimeSpan IOauthSessionIdFactory.SessionValidFor => TimeSpan.FromSeconds(_config.TokenLifeTimeSeconds); + + /// + string IOauthSessionIdFactory.TokenType => "Bearer"; + + /// + bool ISessionIdFactory.CanService(IHttpEvent entity) + { + return entity.Server.HasAuthorization(out _); + } + + /// + 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 diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs deleted file mode 100644 index 6ae2af4..0000000 --- a/libs/VNLib.Plugins.Sessions.OAuth/src/TokenAndSessionIdResult.cs +++ /dev/null @@ -1,41 +0,0 @@ -/* -* 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; - } - } -} -- cgit