diff options
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.OAuth')
5 files changed, 153 insertions, 77 deletions
diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs index 271328a..c87c761 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs @@ -1,16 +1,21 @@ using System; using System.Net; +using System.Text.Json; using VNLib.Utils.Memory; +using VNLib.Hashing.IdentityUtility; using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Extensions; -using VNLib.Plugins.Extensions.Validation; using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Extensions.Loading.Sql; - +using VNLib.Plugins.Extensions.Validation; namespace VNLib.Plugins.Essentials.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 @@ -18,9 +23,11 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints internal sealed class AccessTokenEndpoint : ResourceEndpointBase { - private readonly Lazy<ITokenManager> TokenStore; + private readonly CreateTokenImpl CreateToken; private readonly Applications Applications; + private readonly Task<JsonDocument?> JWTVerificationKey; + //override protection settings to allow most connections to authenticate protected override ProtectionSettings EndpointProtectionSettings { get; } = new() { @@ -29,12 +36,14 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints VerifySessionCors = false }; - public AccessTokenEndpoint(string path, PluginBase pbase, Lazy<ITokenManager> tokenStore) + public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore, Task<JsonDocument?> verificationKey) { InitPathAndLog(path, pbase.Log); - TokenStore = tokenStore; + CreateToken = tokenStore; Applications = new(pbase.GetContextOptions(), pbase.GetPasswords()); + JWTVerificationKey = verificationKey; } + protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) { @@ -43,66 +52,117 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints { //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)) + + //See if we have an application authorized with JWT + else if (entity.RequestArgs.IsArgumentSet("grant_type", "application")) { - 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)) + if(entity.RequestArgs.TryGetNonEmptyValue("token", out string? appJwt)) { - //respond with error message - entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidRequest, "Invalid client_secret"); - return VfReturnType.VirtualSkip; + //Try to get and verify the app + UserApplication? app = GetApplicationFromJwt(appJwt); + + //generate token + return await GenerateTokenAsync(entity, app); } - //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) + } + + //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)) { - entity.CloseResponseError(HttpStatusCode.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application"); - return VfReturnType.VirtualSkip; + + 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 + PrivateString secretPv = new(secret, false); + + //Get the application from apps store + UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv); + + return await GenerateTokenAsync(entity, app); } - //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); + } + + entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type"); + //Default to bad request + return VfReturnType.VirtualSkip; + } + + private UserApplication? GetApplicationFromJwt(string jwtData) + { + //Not enabled + if (JWTVerificationKey.Result == null) + { + return null; + } + + //Parse application token + using JsonWebToken jwt = JsonWebToken.Parse(jwtData); + + //verify the application jwt + if (!jwt.VerifyFromJwk(JWTVerificationKey.Result.RootElement)) + { + 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.ServiceUnavailable, ErrorType.TemporarilyUnabavailable, "You have reached the maximum number of valid tokens for this application"); + return VfReturnType.VirtualSkip; } - //respond with error message - entity.CloseResponseError(HttpStatusCode.UnprocessableEntity, ErrorType.InvalidClient, "The request was missing required arguments"); + + //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; } } diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs index 9a65d62..9eb9928 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs @@ -1,7 +1,7 @@ using System; using VNLib.Net.Http; -using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Sessions.Cache.Client; namespace VNLib.Plugins.Essentials.Sessions.OAuth diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs index e15c6e4..89b36ad 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs @@ -5,6 +5,7 @@ using VNLib.Net.Http; using VNLib.Utils.Logging; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Essentials.Sessions.OAuth; using VNLib.Plugins.Essentials.Sessions.OAuth.Endpoints; using VNLib.Plugins.Extensions.Loading; @@ -43,11 +44,12 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth 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); + //Optional application jwt token + Task<JsonDocument?> jwtTokenSecret = plugin.TryGetSecretAsync("application_token_key") + .ContinueWith(static t => t.Result == null ? null : JsonDocument.Parse(t.Result)); //Init auth endpoint - AccessTokenEndpoint authEp = new(tokenEpPath, plugin, lazyTokenMan); + AccessTokenEndpoint authEp = new(tokenEpPath, plugin, CreateTokenDelegateAsync, jwtTokenSecret); //route auth endpoint plugin.Route(authEp); @@ -56,14 +58,21 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth plugin.Route<RevocationEndpoint>(); //Run - _ = WokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + _ = CacheWokerDoWorkAsync(plugin, localized, cacheConfig, oauth2Config); + } + + private async Task<IOAuth2TokenResult?> CreateTokenDelegateAsync(HttpEntity entity, UserApplication app, CancellationToken cancellation) + { + return await _sessions!.CreateAccessTokenAsync(entity, app, cancellation).ConfigureAwait(false); } /* * Starts and monitors the VNCache connection */ - private async Task WokerDoWorkAsync(PluginBase plugin, ILogProvider localized, IReadOnlyDictionary<string, JsonElement> cacheConfig, IReadOnlyDictionary<string, JsonElement> oauth2Config) + private async Task CacheWokerDoWorkAsync(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); @@ -89,7 +98,6 @@ namespace VNLib.Plugins.Essentials.Sessions.Oauth //Schedule cleanup interval with the plugin scheduler plugin.ScheduleInterval(_sessions, cleanupInterval); - localized.Information("Session provider loaded"); //Run and wait for exit diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs index 2b23721..1058653 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs @@ -1,12 +1,11 @@ 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.Oauth.Applications; using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Sessions.Cache.Client; using static VNLib.Plugins.Essentials.Oauth.OauthSessionExtensions; @@ -90,7 +89,7 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth string sessionId = ComputeSessionIdFromToken(token); //Clear buffer - Utils.Memory.Memory.InitializeBlock(mem.Span); + Memory.InitializeBlock(mem.Span); //Return sessid result return new(sessionId, token, null); diff --git a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs index d938641..79d3789 100644 --- a/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs +++ b/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs @@ -9,9 +9,10 @@ using VNLib.Net.Http; using VNLib.Data.Caching; using VNLib.Data.Caching.Exceptions; using VNLib.Net.Messaging.FBM.Client; +using VNLib.Plugins.Sessions.Cache.Client; using VNLib.Plugins.Essentials.Oauth; using VNLib.Plugins.Essentials.Oauth.Tokens; -using VNLib.Plugins.Sessions.Cache.Client; +using VNLib.Plugins.Essentials.Oauth.Applications; using VNLib.Plugins.Extensions.Loading.Events; namespace VNLib.Plugins.Essentials.Sessions.OAuth @@ -24,8 +25,10 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth { private static readonly SessionHandle NotFoundHandle = new(null, FileProcessArgs.NotFound, null); - static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10); + + private static readonly TimeSpan BackgroundTimeout = TimeSpan.FromSeconds(10); + private readonly IOauthSessionIdFactory factory; private readonly TokenStore TokenStore; @@ -103,26 +106,26 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth } } ///<inheritdoc/> - async Task<IOAuth2TokenResult?> ITokenManager.CreateAccessTokenAsync(IHttpEvent ev, UserApplication app, CancellationToken cancellation) + public async Task<IOAuth2TokenResult?> CreateAccessTokenAsync(HttpEntity 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) + 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); + await session.WaitAndLoadAsync(ev.Entity, cancellation); try { //Init new session - factory.InitNewSession(session, app, ev); + factory.InitNewSession(session, app, ev.Entity); } finally { - await session.UpdateAndRelease(false, ev); + await session.UpdateAndRelease(false, ev.Entity); } //Init new token result to pass to client return new OAuth2TokenResult() @@ -137,14 +140,20 @@ namespace VNLib.Plugins.Essentials.Sessions.OAuth ///<inheritdoc/> Task ITokenManager.RevokeTokensAsync(IReadOnlyCollection<string> tokens, CancellationToken cancellation) { - throw new NotImplementedException(); + return TokenStore.RevokeTokensAsync(tokens, cancellation); } ///<inheritdoc/> Task ITokenManager.RevokeTokensForAppAsync(string appId, CancellationToken cancellation) { - throw new NotImplementedException(); + return TokenStore.RevokeTokenAsync(appId, cancellation); } - + + + /* + * Interval for remving expired tokens + */ + + ///<inheritdoc/> async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken) { //Calculate valid token time |