aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-10-14 15:56:41 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-10-14 15:56:41 -0400
commite2164d088eb5209e2baecf09fbee06d6326fe910 (patch)
tree4efbb5278078ad4bb7ce1726f8745eb4a2eedb93
parentb7ce7b48168d56931cae337bf1268b067edb7dce (diff)
track core updates & pki multi key
-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
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs72
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs163
8 files changed, 356 insertions, 307 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);
}
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs
new file mode 100644
index 0000000..a941852
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/PkiAuthPublicKey.cs
@@ -0,0 +1,72 @@
+/*
+* Copyright (c) 2023 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts
+* File: PkiAuthPublicKey.cs
+*
+* PkiAuthPublicKey.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger
+* VNLib collection of libraries and utilities.
+*
+* VNLib.Plugins.Essentials.Accounts 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.Accounts 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.Text.Json.Serialization;
+
+using VNLib.Hashing.IdentityUtility;
+
+namespace VNLib.Plugins.Essentials.Accounts.MFA
+{
+ /// <summary>
+ /// A json serializable JWK format public key for PKI authentication
+ /// </summary>
+ public record class PkiAuthPublicKey : IJsonWebKey
+ {
+ [JsonPropertyName("kid")]
+ public string? KeyId { get; set; }
+
+ [JsonPropertyName("kty")]
+ public string? KeyType { get; set; }
+
+ [JsonPropertyName("crv")]
+ public string? Curve { get; set; }
+
+ [JsonPropertyName("x")]
+ public string? X { get; set; }
+
+ [JsonPropertyName("y")]
+ public string? Y { get; set; }
+
+ [JsonPropertyName("alg")]
+ public string Algorithm { get; set; } = string.Empty;
+
+ [JsonIgnore]
+ public JwkKeyUsage KeyUse => JwkKeyUsage.Signature;
+
+ ///<inheritdoc/>
+ public string? GetKeyProperty(string propertyName)
+ {
+ return propertyName switch
+ {
+ "kid" => KeyId,
+ "kty" => KeyType,
+ "crv" => Curve,
+ "x" => X,
+ "y" => Y,
+ "alg" => Algorithm,
+ _ => null,
+ };
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
index bd434ae..f1d14f4 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
@@ -39,12 +39,11 @@ using VNLib.Plugins.Essentials.Sessions;
namespace VNLib.Plugins.Essentials.Accounts.MFA
{
- internal static class UserMFAExtensions
+ public static class UserMFAExtensions
{
public const string WEBAUTHN_KEY_ENTRY = "mfa.fido";
public const string TOTP_KEY_ENTRY = "mfa.totp";
public const string SESSION_SIG_KEY = "mfa.sig";
-
public const string USER_PKI_ENTRY = "mfa.pki";
/// <summary>
@@ -97,7 +96,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <param name="entry">The <see cref="MFAEntry"/> to modify the TOTP configuration of</param>
/// <returns>The raw secret that was encrypted and stored in the <see cref="MFAEntry"/>, to send to the client</returns>
/// <exception cref="OutOfMemoryException"></exception>
- public static byte[] MFAGenreateTOTPSecret(this IUser user, MFAConfig config)
+ internal static byte[] MFAGenreateTOTPSecret(this IUser user, MFAConfig config)
{
_ = config.TOTPConfig ?? throw new NotSupportedException("The loaded configuration does not support TOTP");
//Generate a random key
@@ -118,7 +117,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <returns>True if the user has TOTP configured and code matches against its TOTP secret entry, false otherwise</returns>
/// <exception cref="FormatException"></exception>
/// <exception cref="OutOfMemoryException"></exception>
- public static bool VerifyTOTP(this MFAConfig config, IUser user, uint code)
+ internal static bool VerifyTOTP(this MFAConfig config, IUser user, uint code)
{
//Get the base32 TOTP secret for the user and make sure its actually set
string base32Secret = user.MFAGetTOTPSecret();
@@ -126,13 +125,33 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
{
return false;
}
- //Alloc buffer with zero o
- using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAlloc(base32Secret.Length, true);
- ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
- //Verify the TOTP using the decrypted secret
- bool isValid = count && VerifyTOTP(code, buffer.AsSpan(0, count), config.TOTPConfig);
- //Zero out the buffer
- MemoryUtil.InitializeBlock(buffer.Span);
+
+ int length = base32Secret.Length;
+ bool isValid;
+
+ if (length > 256)
+ {
+ //Alloc buffer with zero o
+ using UnsafeMemoryHandle<byte> buffer = MemoryUtil.UnsafeAllocNearestPage(base32Secret.Length, true);
+
+ ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
+ //Verify the TOTP using the decrypted secret
+ isValid = count && VerifyTOTP(code, buffer.AsSpan(0, count), config.TOTPConfig);
+ //Zero out the buffer
+ MemoryUtil.InitializeBlock(buffer.Span);
+ }
+ else
+ {
+ //stack alloc buffer
+ Span<byte> buffer = stackalloc byte[base32Secret.Length];
+
+ ERRNO count = VnEncoding.TryFromBase32Chars(base32Secret, buffer);
+ //Verify the TOTP using the decrypted secret
+ isValid = count && VerifyTOTP(code, buffer[..(int)count], config.TOTPConfig);
+ //Zero out the buffer
+ MemoryUtil.InitializeBlock(buffer);
+ }
+
return isValid;
}
@@ -202,8 +221,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
#endregion
#region PKI
- const int JWK_KEY_BUFFER_SIZE = 2048;
-
+
/// <summary>
/// Gets a value that determines if the user has PKI enabled
/// </summary>
@@ -220,43 +238,46 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <returns>True if the user has PKI enabled, the key was recovered, the key id matches, and the JWT signature is verified</returns>
public static bool PKIVerifyUserJWT(this IUser user, JsonWebToken jwt, string keyId)
{
- //Recover key data from user, it may not be enabled
- using ReadOnlyJsonWebKey? jwk = RecoverKey(user);
-
- if(jwk == null)
- {
- return false;
- }
+ /*
+ * Since multiple keys can be stored, we need to recover the key that matches the desired key id
+ */
+ PkiAuthPublicKey? pub = PkiGetAllPublicKeys(user)?.FirstOrDefault(p => keyId.Equals(p.KeyId, StringComparison.Ordinal));
- //Confim the key id matches
- if(!keyId.Equals(jwk.KeyId, StringComparison.OrdinalIgnoreCase))
+ if(pub == null)
{
return false;
}
-
+
//verify the jwt
- return jwt.VerifyFromJwk(jwk);
+ return jwt.VerifyFromJwk(pub);
}
-
- public static void PKISetUserKey(this IUser user, IReadOnlyDictionary<string, string>? keyFields)
+
+ /// <summary>
+ /// Stores an array of public keys in the user's account object
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="authKeys">The array of jwk format keys to store for the user</param>
+ public static void PKISetPublicKeys(this IUser user, PkiAuthPublicKey[]? authKeys)
{
- if(keyFields == null)
+ if(authKeys == null || authKeys.Length == 0)
{
user[USER_PKI_ENTRY] = null!;
return;
}
//Serialize the key data
- byte[] keyData = JsonSerializer.SerializeToUtf8Bytes(keyFields, Statics.SR_OPTIONS);
-
- //convert to base32 string before writing user data
- string base64 = Convert.ToBase64String(keyData);
+ byte[] keyData = JsonSerializer.SerializeToUtf8Bytes(authKeys, Statics.SR_OPTIONS);
- //Store key data
- user[USER_PKI_ENTRY] = base64;
+ //convert to base64 string before writing user data
+ user[USER_PKI_ENTRY] = VnEncoding.ToBase64UrlSafeString(keyData, false);
}
- private static ReadOnlyJsonWebKey? RecoverKey(IUser user)
+ /// <summary>
+ /// Gets all public keys stored in the user's account object
+ /// </summary>
+ /// <param name="user"></param>
+ /// <returns>The array of public keys if the exist</returns>
+ public static PkiAuthPublicKey[]? PkiGetAllPublicKeys(this IUser user)
{
string? keyData = user[USER_PKI_ENTRY];
@@ -264,25 +285,67 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
{
return null;
}
+
+ //Alloc bin buffer for base64 conversion
+ using UnsafeMemoryHandle<byte> binBuffer = MemoryUtil.UnsafeAllocNearestPage(keyData.Length, true);
- //Get buffer to recover the key data from
- byte[] buffer = ArrayPool<byte>.Shared.Rent(JWK_KEY_BUFFER_SIZE);
- try
+ //Recover base64 bytes from key data
+ ERRNO bytes = VnEncoding.Base64UrlDecode(keyData, binBuffer.Span);
+ if (!bytes)
{
- //Recover base64 bytes from key data
- ERRNO bytes = VnEncoding.TryFromBase64Chars(keyData, buffer);
- if (!bytes)
- {
- return null;
- }
- //Recover json from the decoded binary data
- return new ReadOnlyJsonWebKey(buffer.AsSpan(0, bytes));
+ return null;
}
- finally
+
+ //Deserialize the the key array
+ return JsonSerializer.Deserialize<PkiAuthPublicKey[]>(binBuffer.AsSpan(0, bytes), Statics.SR_OPTIONS);
+ }
+
+ /// <summary>
+ /// Removes a single pki key by it's id
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="keyId">The id of the key to remove</param>
+ public static void PKIRemovePublicKey(this IUser user, string keyId)
+ {
+ //get all keys
+ PkiAuthPublicKey[]? keys = PkiGetAllPublicKeys(user);
+ if(keys == null)
+ {
+ return;
+ }
+
+ //remove the key
+ keys = keys.Where(k => !keyId.Equals(k.KeyId, StringComparison.Ordinal)).ToArray();
+
+ //store the new key array
+ PKISetPublicKeys(user, keys);
+ }
+
+ /// <summary>
+ /// Adds a single pki key to the user's account object, or overwrites
+ /// and existing key with the same id
+ /// </summary>
+ /// <param name="user"></param>
+ /// <param name="key">The key to add to the list of user-keys</param>
+ public static void PKIAddPublicKey(this IUser user, PkiAuthPublicKey key)
+ {
+ //get all keys
+ PkiAuthPublicKey[]? keys = PkiGetAllPublicKeys(user);
+
+ if (keys == null)
+ {
+ keys = new PkiAuthPublicKey[] { key };
+ }
+ else
{
- MemoryUtil.InitializeBlock(buffer.AsSpan());
- ArrayPool<byte>.Shared.Return(buffer);
+ //remove the key if it already exists, then append the new key
+ keys = keys.Where(k => !key.KeyId.Equals(k.KeyId, StringComparison.Ordinal))
+ .Append(key)
+ .ToArray();
}
+
+ //store the new key array
+ PKISetPublicKeys(user, keys);
}
#endregion
@@ -416,9 +479,9 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
return new(upgradeJwt.Compile(), VnEncoding.ToBase32String(secret));
}
- public static void MfaUpgradeSecret(this in SessionInfo session, string? base32Signature) => session[SESSION_SIG_KEY] = base32Signature!;
+ internal static void MfaUpgradeSecret(this in SessionInfo session, string? base32Signature) => session[SESSION_SIG_KEY] = base32Signature!;
- public static string? MfaUpgradeSecret(this in SessionInfo session) => session[SESSION_SIG_KEY];
+ internal static string? MfaUpgradeSecret(this in SessionInfo session) => session[SESSION_SIG_KEY];
}
readonly record struct MfaUpgradeMessage(string ClientJwt, string SessionKey);