diff options
Diffstat (limited to 'libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs')
-rw-r--r-- | libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs new file mode 100644 index 0000000..0e39c8a --- /dev/null +++ b/libs/VNLib.Plugins.Sessions.OAuth/src/Endpoints/AccessTokenEndpoint.cs @@ -0,0 +1,197 @@ +/* +* Copyright (c) 2022 Vaughn Nugent +* +* Library: VNLib +* Package: VNLib.Plugins.Essentials.Sessions.OAuth +* File: AccessTokenEndpoint.cs +* +* AccessTokenEndpoint.cs is part of VNLib.Plugins.Essentials.Sessions.OAuth which is part of the larger +* VNLib collection of libraries and utilities. +* +* VNLib.Plugins.Essentials.Sessions.OAuth is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as +* published by the Free Software Foundation, either version 3 of the +* License, or (at your option) any later version. +* +* VNLib.Plugins.Essentials.Sessions.OAuth is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see https://www.gnu.org/licenses/. +*/ + +using System; +using System.Net; +using System.Text.Json; + +using VNLib.Utils.Memory; +using VNLib.Hashing.IdentityUtility; +using VNLib.Plugins.Essentials; +using VNLib.Plugins.Essentials.Oauth; +using VNLib.Plugins.Essentials.Endpoints; +using VNLib.Plugins.Essentials.Oauth.Tokens; +using VNLib.Plugins.Essentials.Oauth.Applications; +using VNLib.Plugins.Essentials.Extensions; +using VNLib.Plugins.Extensions.Loading; +using VNLib.Plugins.Extensions.Loading.Sql; +using VNLib.Plugins.Extensions.Validation; + +namespace VNLib.Plugins.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 + /// </summary> + internal sealed class AccessTokenEndpoint : ResourceEndpointBase + { + private readonly CreateTokenImpl CreateToken; + private readonly ApplicationStore Applications; + + private readonly Task<ReadOnlyJsonWebKey?> JWTVerificationKey; + + //override protection settings to allow most connections to authenticate + protected override ProtectionSettings EndpointProtectionSettings { get; } = new() + { + DisableBrowsersOnly = true, + DisableSessionsRequired = true, + DisableVerifySessionCors = true + }; + + public AccessTokenEndpoint(string path, PluginBase pbase, CreateTokenImpl tokenStore) + { + InitPathAndLog(path, pbase.Log); + CreateToken = tokenStore; + Applications = new(pbase.GetContextOptions(), pbase.GetPasswords()); + //Try to get the application token key for verifying signed application JWTs + JWTVerificationKey = pbase.TryGetSecretAsync("application_token_key").ToJsonWebKey(); + } + + + protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) + { + //Check for refresh token + if (entity.RequestArgs.IsArgumentSet("grant_type", "refresh_token")) + { + //process a refresh token + } + + //See if we have an application authorized with JWT + else if (entity.RequestArgs.IsArgumentSet("grant_type", "application")) + { + if(entity.RequestArgs.TryGetNonEmptyValue("token", out string? appJwt)) + { + //Try to get and verify the app + UserApplication? app = GetApplicationFromJwt(appJwt); + + //generate token + return await GenerateTokenAsync(entity, app); + } + } + + //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)) + { + + 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 + using PrivateString secretPv = new(secret, false); + + //Get the application from apps store + UserApplication? app = await Applications.VerifyAppAsync(clientId, secretPv); + + return await GenerateTokenAsync(entity, app); + } + } + + entity.CloseResponseError(HttpStatusCode.BadRequest, ErrorType.InvalidClient, "Invalid grant type"); + //Default to bad request + return VfReturnType.VirtualSkip; + } + + private UserApplication? GetApplicationFromJwt(string jwtData) + { + ReadOnlyJsonWebKey? verificationKey = JWTVerificationKey.GetAwaiter().GetResult(); + + //Not enabled + if (verificationKey == null) + { + return null; + } + + //Parse application token + using JsonWebToken jwt = JsonWebToken.Parse(jwtData); + + //verify the application jwt + if (!jwt.VerifyFromJwk(verificationKey)) + { + 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.TooManyRequests, 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, + 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; + } + } +}
\ No newline at end of file |