From e2164d088eb5209e2baecf09fbee06d6326fe910 Mon Sep 17 00:00:00 2001 From: vnugent Date: Sat, 14 Oct 2023 15:56:41 -0400 Subject: track core updates & pki multi key --- .../src/Endpoints/KeepAliveEndpoint.cs | 18 +- .../src/Endpoints/LoginEndpoint.cs | 90 +++----- .../src/Endpoints/LogoutEndpoint.cs | 9 +- .../src/Endpoints/PasswordResetEndpoint.cs | 28 +-- .../src/Endpoints/PkiLoginEndpoint.cs | 253 +++++++++------------ .../src/Endpoints/ProfileEndpoint.cs | 30 +-- 6 files changed, 171 insertions(+), 257 deletions(-) (limited to 'plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints') diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs index e540405..ac0c8eb 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs @@ -23,11 +23,9 @@ */ using System; -using System.Net; using VNLib.Utils.Extensions; using VNLib.Plugins.Essentials.Endpoints; -using VNLib.Plugins.Essentials.Extensions; using VNLib.Plugins.Extensions.Loading; @@ -51,12 +49,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints InitPathAndLog(path, pbase.Log); } - protected override VfReturnType Get(HttpEntity entity) - { - //Return okay - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; - } + protected override VfReturnType Get(HttpEntity entity) => VirtualOk(entity); //Allow post to update user's credentials protected override VfReturnType Post(HttpEntity entity) @@ -72,16 +65,13 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //reauthorize the client entity.ReAuthorizeClient(webm); - webm.Success = true; - + webm.Success = true; //Send the update message to the client - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Return okay - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; + return VirtualOk(entity); } } } \ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs index 0d10811..b01cc3d 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs @@ -75,20 +75,20 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints private readonly IPasswordHashingProvider Passwords; private readonly MFAConfig MultiFactor; private readonly IUserManager Users; - private readonly uint MaxFailedLogins; - private readonly TimeSpan FailedCountTimeout; + private readonly FailedLoginLockout _lockout; public LoginEndpoint(PluginBase pbase, IConfigScope config) { - string? path = config["path"].GetString(); - FailedCountTimeout = config["failed_count_timeout_sec"].GetTimeSpan(TimeParseType.Seconds); - MaxFailedLogins = config["failed_count_max"].GetUInt32(); + string path = config.GetRequiredProperty("path", p => p.GetString()!); + TimeSpan duration = config["failed_count_timeout_sec"].GetTimeSpan(TimeParseType.Seconds); + uint maxLogins = config["failed_count_max"].GetUInt32(); InitPathAndLog(path, pbase.Log); Passwords = pbase.GetOrCreateSingleton(); Users = pbase.GetOrCreateSingleton(); MultiFactor = pbase.GetConfigElement(); + _lockout = new(maxLogins, duration); } protected override ERRNO PreProccess(HttpEntity entity) @@ -102,8 +102,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Conflict if user is logged in if (entity.IsClientAuthorized(AuthorzationCheckLevel.Any)) { - entity.CloseResponse(HttpStatusCode.Conflict); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, HttpStatusCode.Conflict); } //If mfa is enabled, allow processing via mfa @@ -129,15 +128,13 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(loginMessage != null, "Invalid request data")) { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } //validate the message if (!LmValidator.Validate(loginMessage, webm)) { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); } //Time to get the user @@ -146,12 +143,13 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Make sure account exists if (webm.Assert(user != null, INVALID_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } - + + bool locked = _lockout.CheckOrClear(user, entity.RequestedTimeUtc); + //Make sure the account has not been locked out - if (webm.Assert(!UserLoginLocked(user, entity.RequestedTimeUtc), LOCKED_ACCOUNT_MESSAGE)) + if (webm.Assert(locked == false, LOCKED_ACCOUNT_MESSAGE)) { goto Cleanup; } @@ -167,13 +165,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } //Inc failed login count - user.FailedLoginIncrement(entity.RequestedTimeUtc); + _lockout.Increment(user, entity.RequestedTimeUtc); webm.Result = INVALID_MESSAGE; Cleanup: await user.ReleaseAsync(); - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity ,webm); } catch (UserUpdateException uue) { @@ -185,7 +182,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints private bool LoginUser(HttpEntity entity, LoginMessage loginMessage, IUser user, MfaUpgradeWebm webm) { //Verify password before we tell the user the status of their account for security reasons - if (!Passwords.Verify(user.PassHash, loginMessage.Password)) + if (!Passwords.Verify(user.PassHash!, loginMessage.Password)) { return false; } @@ -274,8 +271,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(request != null, "Invalid request data")) { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } //Recover upgrade jwt @@ -283,8 +279,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(upgradeJwt != null, "Missing required upgrade data")) { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); } //Recover stored signature @@ -292,8 +287,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if(webm.Assert(!string.IsNullOrWhiteSpace(storedSig), MFA_ERROR_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Recover upgrade data from upgrade message @@ -301,8 +295,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(upgrade != null, MFA_ERROR_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //recover user account @@ -310,30 +303,28 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(user != null, MFA_ERROR_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } - bool locked = UserLoginLocked(user, entity.RequestedTimeUtc); + bool locked = _lockout.CheckOrClear(user, entity.RequestedTimeUtc); //Make sure the account has not been locked out - if (!webm.Assert(locked == false, LOCKED_ACCOUNT_MESSAGE)) + if (webm.Assert(locked == false, LOCKED_ACCOUNT_MESSAGE)) { - //process mfa login - LoginMfa(entity, user, request, upgrade, webm); + //Locked, so clear stored signature + entity.Session.MfaUpgradeSecret(null); } else { - //Locked, so clear stored signature - entity.Session.MfaUpgradeSecret(null); + //process mfa login + LoginMfa(entity, user, request, upgrade, webm); } //Update user on clean process await user.ReleaseAsync(); //Close rseponse - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } private void LoginMfa(HttpEntity entity, IUser user, JsonDocument request, MFAUpgrade upgrade, MfaUpgradeWebm webm) @@ -355,7 +346,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints webm.Result = "Please check your code."; //Increment flc and update the user in the store - user.FailedLoginIncrement(entity.RequestedTimeUtc); + _lockout.Increment(user, entity.RequestedTimeUtc); return; } //Valid, complete @@ -386,29 +377,6 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints Log.Verbose("Successful login for user {uid}...", user.UserID[..8]); } - public bool UserLoginLocked(IUser user, DateTimeOffset now) - { - //Recover last counter value - TimestampedCounter flc = user.FailedLoginCount(); - - if (flc.Count < MaxFailedLogins) - { - //Period exceeded - return false; - } - - //See if the flc timeout period has expired - if (flc.LastModified.Add(FailedCountTimeout) < now) - { - //clear flc flag - user.ClearFailedLoginCount(); - return false; - } - - //Count has been exceeded, and has not timed out yet - return true; - } - private sealed class MfaUpgradeWebm : ValErrWebMessage { diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs index e5adb17..09b5532 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs @@ -22,9 +22,6 @@ * along with this program. If not, see https://www.gnu.org/licenses/. */ -using System; -using System.Net; - using VNLib.Plugins.Extensions.Loading; using VNLib.Plugins.Essentials.Endpoints; @@ -56,16 +53,14 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (entity.IsClientAuthorized(AuthorzationCheckLevel.Critical)) { entity.InvalidateLogin(); - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; } else { //Detatch the session to cause client only invalidation entity.Session.Detach(); - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; } + + return VirtualOk(entity); } } } diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs index 10eff17..6f8cb77 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs @@ -102,20 +102,19 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints protected override async ValueTask PostAsync(HttpEntity entity) { ValErrWebMessage webm = new(); + //get the request body using PasswordResetMesage? pwReset = await entity.GetJsonFromFileAsync(); if (webm.Assert(pwReset != null, "No request specified")) { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.BadRequest); } //Validate if(!ResetMessValidator.Validate(pwReset, webm)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //get the user's entry in the table @@ -123,23 +122,20 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if(webm.Assert(user != null, "An error has occured, please log-out and try again")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Make sure the account's origin is a local profile if (webm.Assert(user.IsLocalAccount(), "External accounts cannot be modified")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Verify the user's old password if (!Passwords.Verify(user.PassHash, pwReset.Current.AsSpan())) { webm.Result = "Please check your current password"; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Check if totp is enabled @@ -150,8 +146,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //TOTP code is required if(webm.Assert(pwReset.TotpCode.HasValue, "TOTP is enabled on this user account, you must enter your TOTP code.")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Veriy totp code @@ -159,8 +154,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(verified, "Please check your TOTP code and try again")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } } //continue @@ -174,8 +168,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints { //error webm.Result = "Your password could not be updated"; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Publish to user database @@ -184,8 +177,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //delete the user's MFA entry so they can re-enable it webm.Result = "Your password has been updated"; webm.Success = true; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } private sealed class PasswordResetMesage : PrivateStringManager diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs index 42c4ba6..6d9e049 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs @@ -63,17 +63,11 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints private static JwtLoginValidator LwValidator { get; } = new(); private static IValidator AuthValidator { get; } = AuthenticationInfo.GetValidator(); - private static IValidator UserJwkValidator { get; } = GetKeyValidator(); + private static IValidator UserJwkValidator { get; } = GetKeyValidator(); private readonly JwtEndpointConfig _config; private readonly IUserManager _users; - - - /* - * Default protections sessions should be fine (most strict) - * No cross-site/cross origin/bad referrer etc - */ - //protected override ProtectionSettings EndpointProtectionSettings { get; } = new(); + private readonly FailedLoginLockout _lockout; public PkiLoginEndpoint(PluginBase plugin, IConfigScope config) @@ -84,6 +78,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Load config _config = config.DeserialzeAndValidate(); _users = plugin.GetOrCreateSingleton(); + _lockout = new((uint)_config.MaxFailedLogins, TimeSpan.FromSeconds(_config.FailedCountTimeoutSec)); Log.Verbose("PKI endpoint enabled"); } @@ -98,8 +93,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Conflict if user is logged in if (entity.IsClientAuthorized(AuthorzationCheckLevel.Any)) { - entity.CloseResponse(HttpStatusCode.Conflict); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, HttpStatusCode.Conflict); } ValErrWebMessage webm = new(); @@ -109,15 +103,13 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if(webm.Assert(login != null, INVALID_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Validate login message if(!LwValidator.Validate(login, webm)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } IUser? user = null; @@ -130,55 +122,30 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints catch (KeyNotFoundException) { webm.Result = INVALID_MESSAGE; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } catch (FormatException) { webm.Result = INVALID_MESSAGE; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } try { - AuthenticationInfo authInfo; + AuthenticationInfo authInfo = default; + + //Get auth info from jwt + bool isValidAuth = GetAuthInfo(jwt, entity.RequestedTimeUtc, ref authInfo); - //Get the signed payload message - using (JsonDocument payload = jwt.GetPayload()) + if(webm.Assert(isValidAuth, INVALID_MESSAGE)) { - long unixSec = payload.RootElement.GetProperty("iat").GetInt64(); - - DateTimeOffset clientIat = DateTimeOffset.FromUnixTimeSeconds(unixSec); - - if (clientIat.Add(_config.MaxJwtTimeDifference) < entity.RequestedTimeUtc) - { - webm.Result = INVALID_MESSAGE; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - if (clientIat.Subtract(_config.MaxJwtTimeDifference) > entity.RequestedTimeUtc) - { - webm.Result = INVALID_MESSAGE; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //Recover the authenticator information - authInfo = new() - { - EmailAddress = payload.RootElement.GetPropString("sub"), - KeyId = payload.RootElement.GetPropString("keyid"), - SerialNumber = payload.RootElement.GetPropString("serial"), - }; + return VirtualOk(entity, webm); } //Validate auth info if (!AuthValidator.Validate(authInfo, webm)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Get the user from the email address @@ -186,40 +153,35 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints if (webm.Assert(user != null, INVALID_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Check failed login count - if(webm.Assert(UserLoginLocked(user, entity.RequestedTimeUtc) == false, INVALID_MESSAGE)) + if(webm.Assert(_lockout.CheckOrClear(user, entity.RequestedTimeUtc) == false, INVALID_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Now we can verify the signed message against the stored key - if (webm.Assert(user.PKIVerifyUserJWT(jwt, authInfo.KeyId) == true, INVALID_MESSAGE)) + if (webm.Assert(user.PKIVerifyUserJWT(jwt, authInfo.KeyId!) == true, INVALID_MESSAGE)) { //increment flc on invalid signature - user.FailedLoginIncrement(entity.RequestedTimeUtc); + _lockout.Increment(user, entity.RequestedTimeUtc); await user.ReleaseAsync(); - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Account status must be active if(webm.Assert(user.Status == UserStatus.Active, INVALID_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Must be local account if (webm.Assert(user.IsLocalAccount(), INVALID_MESSAGE)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //User is has been authenticated @@ -239,11 +201,14 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints }; //Close response, user is now logged-in - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } catch { + /* + * If an internal error occurs after the authorization has been + * generated, we need to clear the login state that has been created. + */ entity.InvalidateLogin(); throw; } @@ -254,9 +219,31 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } } - /* - * This endpoint also enables - */ + protected override async ValueTask GetAsync(HttpEntity entity) + { + //This endpoint requires valid authorization + if (!entity.IsClientAuthorized(AuthorzationCheckLevel.Critical)) + { + return VirtualClose(entity, HttpStatusCode.Unauthorized); + } + + ValErrWebMessage webm = new(); + + //Get current user + using IUser? user = await _users.GetUserFromIDAsync(entity.Session.UserID); + + if (webm.Assert(user != null, "User account is invalid")) + { + return VirtualOk(entity); + } + + //Get the uesr's stored keys + webm.Result = user.PkiGetAllPublicKeys(); + webm.Success = true; + + return VirtualOk(entity, webm); + } + protected override async ValueTask PatchAsync(HttpEntity entity) { //Check for config flag @@ -267,29 +254,23 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //This endpoint requires valid authorization if (!entity.IsClientAuthorized(AuthorzationCheckLevel.Critical)) { - entity.CloseResponse(HttpStatusCode.Unauthorized); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, HttpStatusCode.Unauthorized); } ValErrWebMessage webm = new(); //Get the request body - using JsonDocument? request = await entity.GetJsonFromFileAsync(); + PkiAuthPublicKey? pubKey = await entity.GetJsonFromFileAsync(); - if(webm.Assert(request != null, "The request message is not valid")) - { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; + if(webm.Assert(pubKey != null, "The request message is not valid")) + { + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); } - //Get the jwk from the request body - using ReadOnlyJsonWebKey jwk = new(request.RootElement); - //Validate the user's jwk - if(!UserJwkValidator.Validate(jwk, webm)) + if(!UserJwkValidator.Validate(pubKey, webm)) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Get the user account @@ -298,50 +279,42 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Confirm not null, this should only happen if user is removed from table while still logged in if(webm.Assert(user != null, "You may not configure PKI authentication")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Local account is required if (webm.Assert(user.IsLocalAccount(), "You do not have a local account, you may not configure PKI authentication")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } try { //Try to get the ECDA instance to confirm the key data could be recovered properly - using ECDsa? testAlg = jwk.GetECDsaPublicKey(); + using ECDsa? testAlg = pubKey.GetECDsaPublicKey(); if (webm.Assert(testAlg != null, "Your JWK is not valid")) { webm.Result = "Your JWK is not valid"; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } } catch(Exception ex) { Log.Debug(ex); webm.Result = "Your JWK is not valid"; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //Extract the user's EC key minimum parameters - IReadOnlyDictionary keyParams = ExtractKeyData(jwk); + return VirtualOk(entity, webm); + } - //Update user's key params - user.PKISetUserKey(keyParams); + //Update user's key, or add it if it doesn't exist + user.PKIAddPublicKey(pubKey); //publish changes await user.ReleaseAsync(); webm.Result = "Successfully updated your PKI authentication method"; webm.Success = true; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } protected override async ValueTask DeleteAsync(HttpEntity entity) @@ -355,8 +328,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //This endpoint requires valid authorization if (!entity.IsClientAuthorized(AuthorzationCheckLevel.Critical)) { - entity.CloseResponse(HttpStatusCode.Unauthorized); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, HttpStatusCode.Unauthorized); } ValErrWebMessage webm = new(); @@ -367,63 +339,63 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Confirm not null, this should only happen if user is removed from table while still logged in if (webm.Assert(user != null, "You may not configure PKI authentication")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } //Local account is required if (webm.Assert(user.IsLocalAccount(), "You do not have a local account, you may not configure PKI authentication")) { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } - //Remove the key - user.PKISetUserKey(null); + //try to get a single key id to delete + if(entity.QueryArgs.TryGetValue("id", out string? keyId)) + { + //Remove only the specified key + user.PKIRemovePublicKey(keyId); + webm.Result = "You have successfully removed the key from your account"; + } + else + { + //Delete all keys + user.PKISetPublicKeys(null); + webm.Result = "You have successfully disabled PKI login"; + } + await user.ReleaseAsync(); - webm.Result = "You have successfully disabled PKI login"; webm.Success = true; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + return VirtualOk(entity, webm); } - public bool UserLoginLocked(IUser user, DateTimeOffset now) + private bool GetAuthInfo(JsonWebToken jwt, DateTimeOffset now, ref AuthenticationInfo authInfo) { - //Recover last counter value - TimestampedCounter flc = user.FailedLoginCount(); + //Get the signed payload message + using JsonDocument payload = jwt.GetPayload(); + + long unixSec = payload.RootElement.GetProperty("iat").GetInt64(); + + DateTimeOffset clientIat = DateTimeOffset.FromUnixTimeSeconds(unixSec); - if (flc.Count < _config.MaxFailedLogins) + if (clientIat.Add(_config.MaxJwtTimeDifference) < now) { - //Period exceeded return false; } - //See if the flc timeout period has expired - if (flc.LastModified.AddSeconds(_config.FailedCountTimeoutSec) < now) + if (clientIat.Subtract(_config.MaxJwtTimeDifference) > now) { - //clear flc flag - user.ClearFailedLoginCount(); return false; } - //Count has been exceeded, and has not timed out yet - return true; - } - - private static IReadOnlyDictionary ExtractKeyData(ReadOnlyJsonWebKey key) - { - Dictionary keyData = new(); - - keyData["kty"] = key.KeyType!; - keyData["use"] = "sig"; - keyData["crv"] = key.GetKeyProperty("crv")!; - keyData["kid"] = key.KeyId!; - keyData["alg"] = key.Algorithm!; - keyData["x"] = key.GetKeyProperty("x")!; - keyData["y"] = key.GetKeyProperty("y")!; + //Recover the authenticator information + authInfo = new() + { + EmailAddress = payload.RootElement.GetPropString("sub"), + KeyId = payload.RootElement.GetPropString("keyid"), + SerialNumber = payload.RootElement.GetPropString("serial"), + }; - return keyData; + return true; } private sealed class JwtLoginValidator : ClientSecurityMessageValidator @@ -529,21 +501,16 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } } - private static IValidator GetKeyValidator() + private static IValidator GetKeyValidator() { - InlineValidator val = new(); + InlineValidator val = new(); val.RuleFor(a => a.KeyType) .NotEmpty() .Must(kt => "EC".Equals(kt, StringComparison.Ordinal)) .WithMessage("The supplied key is not an EC curve key"); - val.RuleFor(a => a.Use) - .NotEmpty() - .Must(u => "sig".Equals(u, StringComparison.OrdinalIgnoreCase)) - .WithMessage("Your key must be configured for signatures"); - - val.RuleFor(a => a.GetKeyProperty("crv")) + val.RuleFor(a => a.Curve) .NotEmpty() .WithName("crv") .Must(p => AllowedCurves.Contains(p, StringComparer.Ordinal)) @@ -556,11 +523,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints val.RuleFor(a => a.Algorithm) .NotEmpty() + .WithName("alg") .Must(a => AllowedAlgs.Contains(a, StringComparer.Ordinal)) .WithMessage("Your key's signature algorithm is not supported"); //Confirm the x axis parameter is valid - val.RuleFor(a => a.GetKeyProperty("x")) + val.RuleFor(a => a.X) .NotEmpty() .WithName("x") .Length(10, 200) @@ -568,9 +536,8 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints .IllegalCharacters() .WithMessage("Your key's X EC point public key parameter conatins invaid characters"); - //Confirm the y axis point is valid - val.RuleFor(a => a.GetKeyProperty("y")) + val.RuleFor(a => a.Y) .NotEmpty() .WithName("y") .Length(10, 200) diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs index 7dfb8a7..22cde19 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs @@ -59,13 +59,14 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints { //get user data from database using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); + //Make sure the account exists if (user == null || user.Status != UserStatus.Active) { //Account was not found - entity.CloseResponse(HttpStatusCode.NotFound); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, HttpStatusCode.NotFound); } + //Get the stored profile AccountData? profile = user.GetProfile(); //No profile found, so return an empty "profile" @@ -76,9 +77,9 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //created time in rfc1123 gmt time Created = user.Created.ToString("R") }; + //Serialize the profile and return to user - entity.CloseResponseJson(HttpStatusCode.OK, profile); - return VfReturnType.VirtualSkip; + return VirtualOkJson(entity, profile); } protected override async ValueTask PostAsync(HttpEntity entity) { @@ -89,41 +90,42 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints AccountData? updateMessage = await entity.GetJsonFromFileAsync(SR_OPTIONS); if (webm.Assert(updateMessage != null, "Malformatted payload")) { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, HttpStatusCode.BadRequest); } + //Validate the new account data if (!AccountValidations.AccountDataValidator.Validate(updateMessage, webm)) { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.UnprocessableEntity); } + //Get the user from database using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); //Make sure the user exists if (webm.Assert(user != null, "Account does not exist")) { //Should probably log the user out here - entity.CloseResponseJson(HttpStatusCode.NotFound, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.NotFound); } + //Overwite the current profile data (will also sanitize inputs) user.SetProfile(updateMessage); //Update the user only if successful await user.ReleaseAsync(); + webm.Result = "Successfully updated account"; webm.Success = true; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; + + return VirtualOk(entity, webm); } //Catch an account update exception catch (UserUpdateException uue) { Log.Error(uue, "An error occured while the user account is being updated"); + //Return message to client webm.Result = "An error occured while updating your account, try again later"; - entity.CloseResponseJson(HttpStatusCode.InternalServerError, webm); - return VfReturnType.VirtualSkip; + return VirtualClose(entity, webm, HttpStatusCode.InternalServerError); } } } -- cgit