diff options
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.OAuth/src')
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 "$(TargetDir)" "F:\Programming\VNLib\devplugins\RuntimeAssets\$(TargetName)" /E /Y /R" /> + </Target> + + <Target Name="PreBuild" BeforeTargets="PreBuildEvent"> + <Exec Command="erase "F:\Programming\VNLib\devplugins\RuntimeAssets\$(TargetName)" /q > nul" /> + </Target> + +</Project> |