aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs18
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs90
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs9
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs28
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs253
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs30
6 files changed, 171 insertions, 257 deletions
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<ManagedPasswordHashing>();
Users = pbase.GetOrCreateSingleton<UserManager>();
MultiFactor = pbase.GetConfigElement<MFAConfig>();
+ _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<VfReturnType> PostAsync(HttpEntity entity)
{
ValErrWebMessage webm = new();
+
//get the request body
using PasswordResetMesage? pwReset = await entity.GetJsonFromFileAsync<PasswordResetMesage>();
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<AuthenticationInfo> AuthValidator { get; } = AuthenticationInfo.GetValidator();
- private static IValidator<ReadOnlyJsonWebKey> UserJwkValidator { get; } = GetKeyValidator();
+ private static IValidator<PkiAuthPublicKey> 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<JwtEndpointConfig>();
_users = plugin.GetOrCreateSingleton<UserManager>();
+ _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<VfReturnType> 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<VfReturnType> 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<PkiAuthPublicKey>();
- 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<string, string> 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<VfReturnType> 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<string, string> ExtractKeyData(ReadOnlyJsonWebKey key)
- {
- Dictionary<string, string> 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<JwtLoginMessage>
@@ -529,21 +501,16 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
}
}
- private static IValidator<ReadOnlyJsonWebKey> GetKeyValidator()
+ private static IValidator<PkiAuthPublicKey> GetKeyValidator()
{
- InlineValidator<ReadOnlyJsonWebKey> val = new();
+ InlineValidator<PkiAuthPublicKey> 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<VfReturnType> PostAsync(HttpEntity entity)
{
@@ -89,41 +90,42 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
AccountData? updateMessage = await entity.GetJsonFromFileAsync<AccountData>(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);
}
}
}