diff options
author | vman <public@vaughnnugent.com> | 2022-10-30 02:28:12 -0400 |
---|---|---|
committer | vman <public@vaughnnugent.com> | 2022-10-30 02:28:12 -0400 |
commit | a8510fb835dcc5e1142d700164ce5a4bd44e1a25 (patch) | |
tree | 28caab320f777a384cb6883b68dd999cdc8c0a3f /Libs/VNLib.Plugins.Essentials.Sessions.OAuth |
Add project files.
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.OAuth')
11 files changed, 775 insertions, 0 deletions
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 +{ + /// <summary> + /// Grants authorization to OAuth2 clients to protected resources + /// with access tokens + /// </summary> + internal sealed class AccessTokenEndpoint : ResourceEndpointBase + { + + private readonly Lazy<ITokenManager> 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<ITokenManager> tokenStore) + { + InitPathAndLog(path, pbase.Log); + TokenStore = tokenStore; + Applications = new(pbase.GetContextOptions(), pbase.GetPasswords()); + } + + protected override async ValueTask<VfReturnType> 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 +{ + /// <summary> + /// An OAuth2 authorized endpoint for revoking the access token + /// held by the current connection + /// </summary> + [ConfigurationName("oauth2")] + internal class RevocationEndpoint : O2EndpointBase + { + + public RevocationEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> 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 + { + /// <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; } + } +} 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<SessionHandle> ISessionProvider.GetSessionAsync(IHttpEvent entity, CancellationToken cancellationToken) + { + return _sessions!.GetSessionAsync(entity, cancellationToken); + } + + + void IRuntimeSessionProvider.Load(PluginBase plugin, ILogProvider localized) + { + //Try get vncache config element + IReadOnlyDictionary<string, JsonElement> cacheConfig = plugin.GetConfig(VNCACHE_CONFIG_KEY); + + IReadOnlyDictionary<string, JsonElement> 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<ITokenManager> lazyTokenMan = new(() => _sessions!, false); + + //Init auth endpoint + AccessTokenEndpoint authEp = new(tokenEpPath, plugin, lazyTokenMan); + + //route auth endpoint + plugin.Route(authEp); + + //Route revocation endpoint + plugin.Route<RevocationEndpoint>(); + + //Run + _ = WokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + } + + /* + * Starts and monitors the VNCache connection + */ + + private async Task WokerDoWorkAsync(PluginBase plugin, ILogProvider localized, IReadOnlyDictionary<string, JsonElement> cacheConfig, IReadOnlyDictionary<string, JsonElement> 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 +{ + /// <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="FBMClient"/> 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, FBMClient 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.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 +{ + /// <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 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<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 + Utils.Memory.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.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 +{ + + /// <summary> + /// Provides OAuth2 session management + /// </summary> + 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); + } + + ///<inheritdoc/> + protected override RemoteSession SessionCtor(string sessionId) => new OAuth2Session(sessionId, Client, BackgroundTimeout, InvlidatateCache); + + private void InvlidatateCache(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; + } + + //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/> + async Task<IOAuth2TokenResult?> 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, + }; + } + ///<inheritdoc/> + Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation) + { + throw new NotImplementedException(); + } + ///<inheritdoc/> + 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<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.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 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFramework>net6.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + <Nullable>enable</Nullable> + <PlatformTarget>x64</PlatformTarget> + <GenerateDocumentationFile>False</GenerateDocumentationFile> + <Authors>Vaughn Nugent</Authors> + <Copyright>Copyright © 2022 Vaughn Nugent</Copyright> + + <EnableDynamicLoading>true</EnableDynamicLoading> + + </PropertyGroup> + + <ItemGroup> + <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Loading.Sql\VNLib.Plugins.Extensions.Loading.Sql.csproj" /> + <ProjectReference Include="..\..\..\Extensions\VNLib.Plugins.Extensions.Validation\VNLib.Plugins.Extensions.Validation.csproj" /> + <ProjectReference Include="..\..\..\Oauth Plugins\VNLib.Plugins.Essentials.Oauth\VNLib.Plugins.Essentials.Oauth.csproj" /> + <ProjectReference Include="..\..\Libs\VNLib.Plugins.Sessions.Cache.Client\VNLib.Plugins.Sessions.Cache.Client.csproj" /> + <ProjectReference Include="..\VNLib.Plugins.Essentials.Sessions.Runtime\VNLib.Plugins.Essentials.Sessions.Runtime.csproj" /> + </ItemGroup> + + <Target Name="PostBuild" AfterTargets="PostBuildEvent"> + <Exec Command="start xcopy "$(TargetDir)" "F:\Programming\Web Plugins\DevPlugins\SessionProviders\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> |