From fdb055f4687c59c5bd0859388dace05766f7ce06 Mon Sep 17 00:00:00 2001 From: vman Date: Fri, 4 Nov 2022 22:12:55 -0400 Subject: Jwt/jwk support, runtime provider updates --- .../Endpoints/AccessTokenEndpoint.cs | 174 ++++++++++++++------- 1 file changed, 117 insertions(+), 57 deletions(-) (limited to 'Libs/VNLib.Plugins.Essentials.Sessions.OAuth/Endpoints') 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 CreateTokenImpl(HttpEntity ev, UserApplication application, CancellationToken cancellation = default); + /// /// 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 TokenStore; + private readonly CreateTokenImpl CreateToken; private readonly Applications Applications; + private readonly Task 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 tokenStore) + public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore, Task verificationKey) { InitPathAndLog(path, pbase.Log); - TokenStore = tokenStore; + CreateToken = tokenStore; Applications = new(pbase.GetContextOptions(), pbase.GetPasswords()); + JWTVerificationKey = verificationKey; } + protected override async ValueTask 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 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; } } -- cgit