aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Sessions.OAuth
diff options
context:
space:
mode:
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.OAuth')
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs174
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/IOauthSessionIdFactory.cs2
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/O2SessionProviderEntry.cs20
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionIdProvider.cs5
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/OAuth2SessionProvider.cs29
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