aboutsummaryrefslogtreecommitdiff
path: root/Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints
diff options
context:
space:
mode:
Diffstat (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints')
-rw-r--r--Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints/AccessTokenEndpoint.cs174
1 files changed, 117 insertions, 57 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;
}
}