From a8510fb835dcc5e1142d700164ce5a4bd44e1a25 Mon Sep 17 00:00:00 2001 From: vman Date: Sun, 30 Oct 2022 02:28:12 -0400 Subject: Add project files. --- .../Endpoints/AccessTokenEndpoint.cs | 109 ++++++++++++ .../Endpoints/RevocationEndpoint.cs | 31 ++++ .../IOauthSessionIdFactory.cs | 38 +++++ .../O2SessionProviderEntry.cs | 117 +++++++++++++ .../OAuth2Session.cs | 107 ++++++++++++ .../OAuth2SessionIdProvider.cs | 109 ++++++++++++ .../OAuth2SessionProvider.cs | 183 +++++++++++++++++++++ .../OAuth2TokenResult.cs | 13 ++ .../OauthTokenResponseMessage.cs | 20 +++ .../TokenAndSessionIdResult.cs | 20 +++ .../VNLib.Plugins.Essentials.Sessions.OAuth.csproj | 28 ++++ 11 files changed, 775 insertions(+) create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2Session.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2TokenResult.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OauthTokenResponseMessage.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/TokenAndSessionIdResult.cs create mode 100644 Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.OAuth') diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs new file mode 100644 index 0000000..271328a --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs @@ -0,0 +1,109 @@ +using System; +using System.Net; + +using VNLib.Utils.Memory; +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Validation; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Sql; + + +namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints +{ + /// + /// Grants authorization to OAuth2 clients to protected resources + /// with access tokens + /// + internal sealed class AccessTokenEndpoint : ResourceEndpointBase + { + + private readonly Lazy TokenStore; + private readonly Applications Applications; + + //override protection settings to allow most connections to authenticate + protected override ProtectionSettings EndpointProtectionSettings { get; } = new() + { + BrowsersOnly = false, + SessionsRequired = false, + VerifySessionCors = false + }; + + public AccessTokenEndpoint(string path, PluginBase pbase, Lazy tokenStore) + { + InitPathAndLog(path, pbase.Log); + TokenStore = tokenStore; + Applications = new(pbase.GetContextOptions(), pbase.GetPasswords()); + } + + protected override async ValueTask PostAsync(HttpEntity entity) + { + //Check for refresh token + if (entity.RequestArgs.IsArgumentSet("grant_type", "refresh_token")) + { + //process a refresh token + } + //Check for grant_type parameter from the request body + if (!entity.RequestArgs.IsArgumentSet("grant_type", "client_credentials")) + { + entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type"); + //Default to bad request + return VfReturnType.VirtualSkip; + } + //Get client id and secret (and make sure theyre not empty + if (entity.RequestArgs.TryGetNonEmptyValue("client_id", out string? clientId) && + entity.RequestArgs.TryGetNonEmptyValue("client_secret", out string? secret)) + { + 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 + PrivateString secretPv = new(secret, false); + //Get the application from apps store + UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv); + if (app == null) + { + //App was not found or the credentials do not match + entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The client credentials are invalid"); + return VfReturnType.VirtualSkip; + } + //Create a new session + IOAuth2TokenResult? result = await TokenStore.Value.CreateAccessTokenAsync(entity.Entity, app, entity.EventCancellation); + if (result == null) + { + entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, 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, + + //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; + + } + //respond with error message + entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The request was missing required arguments"); + return VfReturnType.VirtualSkip; + } + } +} \ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs new file mode 100644 index 0000000..095e07e --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/RevocationEndpoint.cs @@ -0,0 +1,31 @@ +using System; +using System.Text.Json; + +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Extensions.Loading.Configuration; + +namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints +{ + /// + /// An OAuth2 authorized endpoint for revoking the access token + /// held by the current connection + /// + [ConfigurationName("oauth2")] + internal class RevocationEndpoint : O2EndpointBase + { + + public RevocationEndpoint(PluginBase pbase, IReadOnlyDictionary config) + { + string? path = config["revocation_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.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs new file mode 100644 index 0000000..9a65d62 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs @@ -0,0 +1,38 @@ +using System; + +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Sessions.Cache.Client; + +namespace VNLib.Plugins.Essentials.Sessions.OAuth +{ + public interface IOauthSessionIdFactory : ISessionIdFactory + { + /// + /// 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 + /// + TimeSpan SessionValidFor { get; } + /// + /// Called when the session provider wishes to generate a new session + /// and required credential information to generate the new session + /// + /// The information genreated for the news ession + TokenAndSessionIdResult GenerateTokensAndId(); + /// + /// The type of token this session provider generates + /// + string TokenType { get; } + } +} diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs new file mode 100644 index 0000000..e15c6e4 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs @@ -0,0 +1,117 @@ + +using System.Text.Json; + +using VNLib.Net.Http; +using VNLib.Utils.Logging; +using VNLib.Utils.Extensions; +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Sessions.OAuth; +using VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Loading.Events; +using VNLib.Plugins.Extensions.Loading.Routing; +using VNLib.Plugins.Extensions.Loading.Configuration; + +namespace VNLib.Plugins.Essentials.Sessions.Oauth +{ + public sealed class O2SessionProviderEntry : IRuntimeSessionProvider + { + const string VNCACHE_CONFIG_KEY = "vncache"; + const string OAUTH2_CONFIG_KEY = "oauth2"; + + private OAuth2SessionProvider? _sessions; + + bool IRuntimeSessionProvider.CanProcess(IHttpEvent entity) + { + //If authorization header is set try to process as oauth2 session + return entity.Server.Headers.HeaderSet(System.Net.HttpRequestHeader.Authorization); + } + + ValueTask ISessionProvider.GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) + { + return _sessions!.GetSessionAsync(entity, cancellationToken); + } + + + void IRuntimeSessionProvider.Load(PluginBase plugin, ILogProvider localized) + { + //Try get vncache config element + IReadOnlyDictionary cacheConfig = plugin.GetConfig(VNCACHE_CONFIG_KEY); + + IReadOnlyDictionary oauth2Config = plugin.GetConfig(OAUTH2_CONFIG_KEY); + + string tokenEpPath = oauth2Config["token_path"].GetString() ?? throw new KeyNotFoundException($"Missing required 'token_path' in '{OAUTH2_CONFIG_KEY}' config"); + + //TODO fix with method that will wait until cache is actually loaded + Lazy lazyTokenMan = new(() => _sessions!, false); + + //Init auth endpoint + AccessTokenEndpoint authEp = new(tokenEpPath, plugin, lazyTokenMan); + + //route auth endpoint + plugin.Route(authEp); + + //Route revocation endpoint + plugin.Route(); + + //Run + _ = WokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + } + + /* + * Starts and monitors the VNCache connection + */ + + private async Task WokerDoWorkAsync(PluginBase plugin, ILogProvider localized, IReadOnlyDictionary cacheConfig, IReadOnlyDictionary oauth2Config) + { + //Init cache client + using VnCacheClient cache = new(plugin.IsDebug() ? plugin.Log : null, Utils.Memory.Memory.Shared); + + try + { + 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); + + //Try loading config + await cache.LoadConfigAsync(plugin, cacheConfig); + + //Init session provider now that client is loaded + _sessions = new(cache.Resource!, cacheLimit, idProv, plugin.GetContextOptions()); + + //Schedule cleanup interval with the plugin scheduler + plugin.ScheduleInterval(_sessions, cleanupInterval); + + + localized.Information("Session provider loaded"); + + //Run and wait for exit + await cache.RunAsync(localized, plugin.UnloadToken); + + } + catch (OperationCanceledException) + {} + catch (KeyNotFoundException e) + { + localized.Error("Missing required configuration variable for VnCache client: {0}", e.Message); + } + catch (Exception ex) + { + localized.Error(ex, "Cache client error occured in session provider"); + } + finally + { + _sessions = null; + } + + localized.Information("Cache client exited"); + } + } +} \ No newline at end of file diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2Session.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2Session.cs new file mode 100644 index 0000000..5987c81 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2Session.cs @@ -0,0 +1,107 @@ +using System; + +using VNLib.Net.Http; +using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Sessions.Cache.Client; + +using static VNLib.Plugins.Essentials.Sessions.ISessionExtensions; + +namespace VNLib.Plugins.Essentials.Sessions.OAuth +{ + /// + /// The implementation of the OAuth2 session container for HTTP sessions + /// + internal sealed class OAuth2Session : RemoteSession + { + private readonly Action InvalidateCache; + + /// + /// 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, FBMClient client, TimeSpan backgroundTimeOut, Action invalidCache) + : base(sessionId, client, backgroundTimeOut) + { + InvalidateCache = invalidCache; + IsInvalid = false; + } + + public bool IsInvalid { get; private set; } + + + /// + /// + 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"); + } + + /// + 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); + } + /// + /// + 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.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs new file mode 100644 index 0000000..2b23721 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs @@ -0,0 +1,109 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +using VNLib.Utils; +using VNLib.Utils.Memory; +using VNLib.Utils.Extensions; +using VNLib.Hashing; +using VNLib.Net.Http; +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Sessions.Cache.Client; +using static VNLib.Plugins.Essentials.Oauth.OauthSessionExtensions; + + +namespace VNLib.Plugins.Essentials.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 ISessionIdFactory.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 = Memory.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 = Memory.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 + Utils.Memory.Memory.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.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs new file mode 100644 index 0000000..d938641 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs @@ -0,0 +1,183 @@ +using System; +using System.Net; + +using Microsoft.EntityFrameworkCore; + +using VNLib.Utils; +using VNLib.Utils.Logging; +using VNLib.Net.Http; +using VNLib.Data.Caching; +using VNLib.Data.Caching.Exceptions; +using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Oauth.Tokens; +using VNLib.Plugins.Sessions.Cache.Client; +using VNLib.Plugins.Extensions.Loading.Events; + +namespace VNLib.Plugins.Essentials.Sessions.OAuth +{ + + /// + /// Provides OAuth2 session management + /// + internal sealed class OAuth2SessionProvider : SessionCacheClient, ISessionProvider, ITokenManager, IIntervalScheduleable + { + + private static readonly SessionHandle NotFoundHandle = new(null, FileProcessArgs.NotFound, null); + static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10); + + private readonly IOauthSessionIdFactory factory; + private readonly TokenStore TokenStore; + + public OAuth2SessionProvider(FBMClient client, int maxCacheItems, IOauthSessionIdFactory idFactory, DbContextOptions dbCtx) + : base(client, maxCacheItems) + { + factory = idFactory; + TokenStore = new(dbCtx); + } + + /// + protected override RemoteSession SessionCtor(string sessionId) => new OAuth2Session(sessionId, Client, BackgroundTimeout, InvlidatateCache); + + private void InvlidatateCache(OAuth2Session session) + { + lock (CacheLock) + { + _ = CacheTable.Remove(session.SessionID); + } + } + + /// + 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) + { + 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; + } + + //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); + } + } + /// + async Task ITokenManager.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, + }; + } + /// + Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection tokens, CancellationToken cancellation) + { + throw new NotImplementedException(); + } + /// + Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation) + { + throw new NotImplementedException(); + } + + async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) + { + //Calculate valid token time + DateTimeOffset validAfter = DateTimeOffset.UtcNow.Subtract(factory.SessionValidFor); + //Remove tokens from db store + IReadOnlyCollection revoked = await TokenStore.CleanupExpiredTokensAsync(validAfter, cancellationToken); + //exception list + List? errors = null; + //Remove all sessions from the store + foreach (ActiveToken token in revoked) + { + try + { + //Remove tokens by thier object id from cache + await base.Client.DeleteObjectAsync(token.Id, cancellationToken); + } + //Ignore if the object has already been removed + catch (ObjectNotFoundException) + {} + catch (Exception ex) + { + errors ??= new(); + errors.Add(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.Essentials.Sessions.OAuth/OAuth2TokenResult.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2TokenResult.cs new file mode 100644 index 0000000..1bff743 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2TokenResult.cs @@ -0,0 +1,13 @@ +using VNLib.Plugins.Essentials.Oauth; + +namespace VNLib.Plugins.Essentials.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.Essentials.Sessions.OAuth/OauthTokenResponseMessage.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OauthTokenResponseMessage.cs new file mode 100644 index 0000000..c80891e --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OauthTokenResponseMessage.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +#nullable enable + +namespace VNLib.Plugins.Essentials.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.Essentials.Sessions.OAuth/TokenAndSessionIdResult.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/TokenAndSessionIdResult.cs new file mode 100644 index 0000000..f8d381f --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/TokenAndSessionIdResult.cs @@ -0,0 +1,20 @@ +#nullable enable + +using VNLib; + +namespace VNLib.Plugins.Essentials.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.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj new file mode 100644 index 0000000..cba7822 --- /dev/null +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/VNLib.Plugins.Essentials.Sessions.OAuth.csproj @@ -0,0 +1,28 @@ + + + + net6.0 + enable + enable + x64 + False + Vaughn Nugent + Copyright © 2022 Vaughn Nugent + + true + + + + + + + + + + + + + + + + -- cgit