aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Accounts
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/AccountsEntryPoint.cs1
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs38
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs28
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs43
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs15
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs39
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs65
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs8
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs17
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAConfig.cs24
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MfaAuthManager.cs35
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TOTPConfig.cs30
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TotpAuthProcessor.cs23
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/UserTotpMfaExtensions.cs64
14 files changed, 217 insertions, 213 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/AccountsEntryPoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/AccountsEntryPoint.cs
index 31b4180..f4eebcc 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/AccountsEntryPoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/AccountsEntryPoint.cs
@@ -86,6 +86,7 @@ namespace VNLib.Plugins.Essentials.Accounts
if (this.HasConfigForType<PkiLoginEndpoint>())
{
this.Route<PkiLoginEndpoint>();
+ Log.Verbose("Public-key login enabled");
}
if (this.HasConfigForType<FidoEndpoint>())
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs
index 1627d8b..877ca3d 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/FidoEndpoint.cs
@@ -36,6 +36,7 @@ using VNLib.Plugins.Essentials.Users;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Essentials.Extensions;
@@ -50,39 +51,24 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
/// This enpdoint requires Fido to be enabled in the MFA configuration.
/// </para>
/// </summary>
+ [EndpointPath("{{path}}")]
+ [EndpointLogName("FIDO")]
[ConfigurationName("fido_endpoint")]
- internal sealed class FidoEndpoint : ProtectedWebEndpoint
+ internal sealed class FidoEndpoint(PluginBase plugin, IConfigScope config) : ProtectedWebEndpoint
{
private static readonly FidoResponseValidator ResponseValidator = new();
private static readonly FidoClientDataJsonValidtor ClientDataValidator = new();
- private readonly IUserManager _users;
- private readonly FidoConfig _fidoConfig;
- private readonly FidoPubkeyAlgorithm[] _supportedAlgs;
-
- public FidoEndpoint(PluginBase plugin, IConfigScope config)
- {
- _users = plugin.GetOrCreateSingleton<UserManager>();
- _fidoConfig = plugin.GetConfigElement<MFAConfig>().FIDOConfig
+ private readonly IUserManager _users = plugin.GetOrCreateSingleton<UserManager>();
+ private readonly FidoConfig _fidoConfig = plugin.GetConfigElement<MFAConfig>().FIDOConfig
?? throw new ConfigurationValidationException("Fido configuration was not set, but Fido endpoint was enabled");
- InitPathAndLog(
- path: config.GetRequiredProperty("path", p => p.GetString()!),
- log: plugin.Log.CreateScope("Fido-Endpoint")
- );
-
- /*
- * For now hard-code supported algorithms,
- * ECDSA is easiest for the time being
- */
-
- _supportedAlgs =
- [
- new FidoPubkeyAlgorithm(algId: -7), //ES256
- new FidoPubkeyAlgorithm(algId: -35), //ES384
- new FidoPubkeyAlgorithm(algId: -36), //ES512
- ];
- }
+ private static readonly FidoPubkeyAlgorithm[] _supportedAlgs =
+ [
+ new FidoPubkeyAlgorithm(algId: -7), //ES256
+ new FidoPubkeyAlgorithm(algId: -35), //ES384
+ new FidoPubkeyAlgorithm(algId: -36), //ES512
+ ];
protected override VfReturnType Get(HttpEntity entity)
{
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs
index ac0c8eb..ec5a49f 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/KeepAliveEndpoint.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Accounts
@@ -27,27 +27,20 @@ using System;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
+ [EndpointPath("{{path}}")]
+ [EndpointLogName("Heartbeat")]
[ConfigurationName("keepalive_endpoint")]
- internal sealed class KeepAliveEndpoint : ProtectedWebEndpoint
+ internal sealed class KeepAliveEndpoint(PluginBase plugin, IConfigScope config) : ProtectedWebEndpoint
{
- readonly TimeSpan tokenRegenTime;
-
- /*
- * Endpoint does not use a log, so IniPathAndLog is never called
- * and path verification happens verbosly
- */
- public KeepAliveEndpoint(PluginBase pbase, IConfigScope config)
- {
- string? path = config["path"].GetString();
-
- tokenRegenTime = config["token_refresh_sec"].GetTimeSpan(TimeParseType.Seconds);
-
- InitPathAndLog(path, pbase.Log);
- }
+ private readonly TimeSpan tokenRegenTime = config.GetRequiredProperty(
+ property: "token_refresh_sec",
+ static p => p.GetTimeSpan(TimeParseType.Seconds)
+ );
protected override VfReturnType Get(HttpEntity entity) => VirtualOk(entity);
@@ -64,8 +57,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
//reauthorize the client
entity.ReAuthorizeClient(webm);
-
- webm.Success = true;
+
//Send the update message to the client
return VirtualOk(entity, webm);
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs
index faad34b..5bc286e 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs
@@ -44,6 +44,7 @@ using VNLib.Plugins.Essentials.Accounts.MFA;
using VNLib.Plugins.Essentials.Accounts.Validators;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using static VNLib.Plugins.Essentials.Statics;
using VNLib.Plugins.Essentials.Accounts.MFA.Totp;
using VNLib.Plugins.Essentials.Accounts.MFA.Fido;
@@ -67,8 +68,10 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
/// <summary>
/// Provides an authentication endpoint for user-accounts
/// </summary>
+ [EndpointPath("{{path}}")]
+ [EndpointLogName("LOGIN")]
[ConfigurationName("login_endpoint")]
- internal sealed class LoginEndpoint : UnprotectedWebEndpoint
+ internal sealed class LoginEndpoint(PluginBase pbase, IConfigScope config) : UnprotectedWebEndpoint
{
public const string INVALID_MESSAGE = "Please check your email or password. You may get locked out.";
public const string LOCKED_ACCOUNT_MESSAGE = "You have been timed out, please try again later";
@@ -76,37 +79,13 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
private static readonly LoginMessageValidation LmValidator = new();
- private readonly MfaAuthManager MultiFactor;
- private readonly IUserManager Users;
- private readonly FailedLoginLockout _lockout;
-
- public LoginEndpoint(PluginBase pbase, IConfigScope config)
- {
- string path = config.GetRequiredProperty("path", p => p.GetString()!);
- TimeSpan duration = config["failed_attempt_timeout_sec"].GetTimeSpan(TimeParseType.Seconds);
- uint maxLogins = config["max_login_attempts"].GetUInt32();
-
- InitPathAndLog(path, pbase.Log);
-
- MFAConfig conf = pbase.GetConfigElement<MFAConfig>();
- Users = pbase.GetOrCreateSingleton<UserManager>();
- _lockout = new(maxLogins, duration);
-
-
- List<IMfaProcessor> proc = [];
-
- if(conf.TOTPEnabled)
- {
- proc.Add(new TotpAuthProcessor(conf.TOTPConfig!));
- }
-
- if(conf.FIDOEnabled)
- {
- proc.Add(new FidoMfaProcessor(conf.FIDOConfig!));
- }
-
- MultiFactor = new(conf, [.. proc]);
- }
+ private readonly MfaAuthManager MultiFactor = pbase.GetOrCreateSingleton<MfaAuthManager>();
+ private readonly IUserManager Users = pbase.GetOrCreateSingleton<UserManager>();
+
+ private readonly FailedLoginLockout _lockout = new(
+ maxCounts: config.GetRequiredProperty<uint>("max_login_attempts"),
+ maxTimeout: config.GetRequiredProperty("failed_attempt_timeout_sec", p => p.GetTimeSpan(TimeParseType.Seconds))
+ );
protected override ERRNO PreProccess(HttpEntity entity)
{
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs
index 09b5532..2726079 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LogoutEndpoint.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Accounts
@@ -24,20 +24,15 @@
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Essentials.Endpoints;
+using VNLib.Plugins.Extensions.Loading.Routing;
namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
+ [EndpointPath("{{path}}")]
+ [EndpointLogName("LOGOUT")]
[ConfigurationName("logout_endpoint")]
- internal class LogoutEndpoint : UnprotectedWebEndpoint
+ internal class LogoutEndpoint(): UnprotectedWebEndpoint
{
-
- public LogoutEndpoint(PluginBase pbase, IConfigScope config)
- {
- string? path = config["path"].GetString();
- InitPathAndLog(path, pbase.Log);
- }
-
-
protected override VfReturnType Post(HttpEntity entity)
{
/*
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs
index f31334c..07051c9 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs
@@ -40,6 +40,7 @@ using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using VNLib.Plugins.Essentials.Accounts.MFA.Otp;
using VNLib.Plugins.Essentials.Accounts.MFA.Totp;
using VNLib.Plugins.Essentials.Accounts.MFA.Fido;
@@ -47,33 +48,24 @@ using VNLib.Plugins.Essentials.Accounts.MFA.Fido;
namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
+ [EndpointPath("{{path}}")]
+ [EndpointLogName("MFA-Endpoint")]
[ConfigurationName("mfa_endpoint")]
- internal sealed class MFAEndpoint : ProtectedWebEndpoint
+ internal sealed class MFAEndpoint(PluginBase pbase) : ProtectedWebEndpoint
{
public const int TOTP_URL_MAX_CHARS = 1024;
private const string CHECK_PASSWORD = "Please check your password";
- private readonly IUserManager Users;
- private readonly MFAConfig MultiFactor;
-
- public MFAEndpoint(PluginBase pbase, IConfigScope config)
- {
- InitPathAndLog(
- path: config.GetRequiredProperty("path", p => p.GetString()!),
- log: pbase.Log.CreateScope("Mfa-Endpoint")
- );
-
- Users = pbase.GetOrCreateSingleton<UserManager>();
- MultiFactor = pbase.GetConfigElement<MFAConfig>();
- }
+ private readonly IUserManager Users = pbase.GetOrCreateSingleton<UserManager>();
+ private readonly MfaAuthManager _mfa = pbase.GetOrCreateSingleton<MfaAuthManager>();
protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity)
{
string[] enabledModes = new string[3];
//Load the MFA entry for the user
- using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID);
-
+ using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID);
+
if (user?.TotpEnabled() == true)
{
enabledModes[0] = "totp";
@@ -86,7 +78,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
if (user?.OtpAuthEnabled() == true)
{
- enabledModes[2] = "pki";
+ enabledModes[2] = "pkotp";
}
//Return mfa modes as an array
@@ -150,7 +142,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
case "totp":
{
//Confirm totp is enabled
- if (webm.Assert(MultiFactor.TOTPEnabled, "TOTP is not enabled on the current server"))
+ if (webm.Assert(_mfa.TotpIsEnabled(), "TOTP is not enabled on the current server"))
{
return VirtualOk(entity, webm);
}
@@ -257,7 +249,8 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
private async Task UpdateUserTotp(HttpEntity entity, IUser user, WebMessage webm)
{
//generate a new secret (passing the buffer which will get copied to an array because the pw bytes can be modified during encryption)
- byte[] secretBuffer = user.MFAGenreateTOTPSecret(MultiFactor);
+ byte[]? secretBuffer = _mfa.TotpSetNewSecret(user);
+
//Alloc output buffer
IMemoryHandle<byte> outputBuffer = MemoryUtil.SafeAlloc(4096, true);
@@ -277,10 +270,10 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
webm.Result = new TOTPUpdateMessage()
{
- Issuer = MultiFactor.TOTPConfig.IssuerName,
- Digits = MultiFactor.TOTPConfig.TOTPDigits,
- Period = (int)MultiFactor.TOTPConfig.TOTPPeriod.TotalSeconds,
- Algorithm = MultiFactor.TOTPConfig.TOTPAlg.ToString(),
+ Issuer = _mfa.Config.TOTPConfig!.IssuerName,
+ Digits = _mfa.Config.TOTPConfig.Digits,
+ Period = (int)_mfa.Config.TOTPConfig.Period.TotalSeconds,
+ Algorithm = _mfa.Config.TOTPConfig.HashAlg.ToString(),
//Convert the secret to base64 string to send to client
Base64EncSecret = Convert.ToBase64String(outputBuffer.Span[..(int)count])
};
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs
index b274f5f..1a28efc 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs
@@ -38,6 +38,7 @@ using VNLib.Plugins.Essentials.Accounts.MFA;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using VNLib.Plugins.Essentials.Accounts.MFA.Totp;
namespace VNLib.Plugins.Essentials.Accounts.Endpoints
@@ -57,41 +58,14 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
/// Password reset for user's that are logged in and know
/// their passwords to reset their MFA methods
/// </summary>
+ [EndpointPath("{{path}}")]
[ConfigurationName("password_endpoint")]
- internal sealed class PasswordChangeEndpoint : ProtectedWebEndpoint
+ internal sealed class PasswordChangeEndpoint(PluginBase pbase, IConfigScope config) : ProtectedWebEndpoint
{
- private readonly IUserManager Users;
- private readonly MFAConfig mFAConfig;
- private readonly IValidator<PasswordResetMesage> ResetMessValidator;
+ private readonly IValidator<PasswordResetMesage> ResetMessValidator = GetMessageValidator();
+ private readonly UserManager Users = pbase.GetOrCreateSingleton<UserManager>();
+ private readonly MfaAuthManager _mfaAuth = pbase.GetOrCreateSingleton<MfaAuthManager>();
- public PasswordChangeEndpoint(PluginBase pbase, IConfigScope config)
- {
- string? path = config["path"].GetString();
- InitPathAndLog(path, pbase.Log);
-
- Users = pbase.GetOrCreateSingleton<UserManager>();
- ResetMessValidator = GetMessageValidator();
- mFAConfig = pbase.GetConfigElement<MFAConfig>();
- }
-
- private static IValidator<PasswordResetMesage> GetMessageValidator()
- {
- InlineValidator<PasswordResetMesage> rules = new();
-
- rules.RuleFor(static pw => pw.Current)
- .NotEmpty()
- .WithMessage("You must specify your current password")
- .Length(8, 100);
-
- //Use centralized password validator for new passwords
- rules.RuleFor(static pw => pw.NewPassword)
- .NotEmpty()
- .NotEqual(static pm => pm.Current)
- .WithMessage("Your new password may not equal your new current password")
- .SetValidator(AccountValidations.PasswordValidator);
-
- return rules;
- }
protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
{
@@ -135,7 +109,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
}
//Check if totp is enabled
- if (mFAConfig.TOTPEnabled && user.TotpEnabled())
+ if (_mfaAuth.TotpIsEnabled() && user.TotpEnabled())
{
//TOTP code is required
if (webm.Assert(pwReset.TotpCode.HasValue, "TOTP is enabled on this user account, you must enter your TOTP code."))
@@ -144,7 +118,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
}
//Veriy totp code
- bool verified = mFAConfig.VerifyTOTP(user, pwReset.TotpCode.Value);
+ bool verified = _mfaAuth.TotpVerifyCode(user, pwReset.TotpCode.Value);
if (webm.Assert(verified, "Please check your TOTP code and try again"))
{
@@ -171,12 +145,27 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
return VirtualOk(entity, webm);
}
- private sealed class PasswordResetMesage : PrivateStringManager
+ private static IValidator<PasswordResetMesage> GetMessageValidator()
{
- public PasswordResetMesage() : base(2)
- {
- }
+ InlineValidator<PasswordResetMesage> rules = new();
+ rules.RuleFor(static pw => pw.Current)
+ .NotEmpty()
+ .WithMessage("You must specify your current password")
+ .Length(8, 100);
+
+ //Use centralized password validator for new passwords
+ rules.RuleFor(static pw => pw.NewPassword)
+ .NotEmpty()
+ .NotEqual(static pm => pm.Current)
+ .WithMessage("Your new password may not equal your new current password")
+ .SetValidator(AccountValidations.PasswordValidator);
+
+ return rules;
+ }
+
+ private sealed class PasswordResetMesage() : PrivateStringManager(2)
+ {
[JsonPropertyName("current")]
public string? Current
{
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs
index 618a053..7d8e9d5 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs
@@ -46,11 +46,12 @@ using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Essentials.Accounts.Validators;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using VNLib.Plugins.Essentials.Accounts.MFA.Otp;
namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
-
+ [EndpointPath("{{path}}")]
[ConfigurationName("pki_auth_endpoint")]
internal sealed class PkiLoginEndpoint : UnprotectedWebEndpoint
{
@@ -78,15 +79,10 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
public PkiLoginEndpoint(PluginBase plugin, IConfigScope config)
{
- string? path = config["path"].GetString();
- InitPathAndLog(path, plugin.Log);
-
//Load config
_config = config.DeserialzeAndValidate<JwtEndpointConfig>();
_users = plugin.GetOrCreateSingleton<UserManager>();
_lockout = new((uint)_config.MaxFailedLogins, TimeSpan.FromSeconds(_config.FailedCountTimeoutSec));
-
- Log.Verbose("PKI endpoint enabled");
}
protected override ERRNO PreProccess(HttpEntity entity)
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs
index 22cde19..b6c4c91 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/ProfileEndpoint.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Accounts
@@ -33,6 +33,7 @@ using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using static VNLib.Plugins.Essentials.Statics;
@@ -41,19 +42,11 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
/// <summary>
/// Provides an http endpoint for user account profile access
/// </summary>
+ [EndpointPath("{{path}}")]
[ConfigurationName("profile_endpoint")]
- internal sealed class ProfileEndpoint : ProtectedWebEndpoint
+ internal sealed class ProfileEndpoint(PluginBase pbase) : ProtectedWebEndpoint
{
- private readonly IUserManager Users;
-
- public ProfileEndpoint(PluginBase pbase, IConfigScope config)
- {
- string? path = config["path"].GetString();
-
- InitPathAndLog(path, pbase.Log);
- //Store user system
- Users = pbase.GetOrCreateSingleton<UserManager>();
- }
+ private readonly UserManager Users = pbase.GetOrCreateSingleton<UserManager>();
protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity)
{
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAConfig.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAConfig.cs
index 7b7a73f..80cb4b1 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAConfig.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAConfig.cs
@@ -23,6 +23,7 @@
*/
using System;
+using System.Collections.Generic;
using System.Text.Json.Serialization;
using FluentValidation;
@@ -86,15 +87,26 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
public int UpgradeKeyBytes { get; set; } = 32;
[JsonIgnore]
- public bool TOTPEnabled => TOTPConfig?.Enabled == true;
-
- [JsonIgnore]
- public bool FIDOEnabled => FIDOConfig != null;
-
- [JsonIgnore]
public TimeSpan UpgradeValidFor { get; private set; } = TimeSpan.FromSeconds(120);
public void OnValidate() => _validator.ValidateAndThrow(this);
+
+ public IMfaProcessor[] GetSupportedProcessors()
+ {
+ List<IMfaProcessor> processors = [];
+
+ if (TOTPConfig?.Enabled == true)
+ {
+ processors.Add(new TotpAuthProcessor(TOTPConfig!));
+ }
+
+ if (FIDOConfig != null)
+ {
+ processors.Add(new FidoMfaProcessor(FIDOConfig));
+ }
+
+ return [.. processors];
+ }
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MfaAuthManager.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MfaAuthManager.cs
index dc52c59..a166083 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MfaAuthManager.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MfaAuthManager.cs
@@ -42,25 +42,37 @@ using VNLib.Plugins.Extensions.Loading;
namespace VNLib.Plugins.Essentials.Accounts.MFA
{
- internal sealed class MfaAuthManager(MFAConfig config, IMfaProcessor[] processors)
+ internal sealed class MfaAuthManager(MFAConfig config)
{
public const string SESSION_SIG_KEY = "mfa.sig";
- private const HashAlg SigAlg = HashAlg.SHA256;
- private static readonly byte[] UpgradeHeader = CompileJwtHeader();
+ private const HashAlg SigAlg = HashAlg.SHA256;
+
+ private readonly IMfaProcessor[] processors = config.GetSupportedProcessors();
+ private readonly byte[] UpgradeHeader = CompileJwtHeader();
+
+ public MfaAuthManager(PluginBase plugin) : this(plugin.GetConfigElement<MFAConfig>())
+ { }
public bool Armed => processors.Length > 0;
/// <summary>
+ /// Gets the MFA processors available for use
+ /// </summary>
+ public IEnumerable<IMfaProcessor> Processors => processors;
+
+ /// <summary>
+ /// Gets the MFA configuration settings
+ /// </summary>
+ public MFAConfig Config => config;
+
+ /// <summary>
/// Determines if the user has any MFA methods enabled and
/// should continue with an MFA upgrade
/// </summary>
/// <param name="user">The user to upgrade the mfa request on</param>
/// <returns>True if the user has any MFA methods enabled</returns>
- public bool HasMfaEnabled(IUser user)
- {
- return processors.Any(p => p.MethodEnabledForUser(user));
- }
+ public bool HasMfaEnabled(IUser user) => processors.Any(p => p.MethodEnabledForUser(user));
/// <summary>
/// Gets the upgrade message to send back to the client to
@@ -156,10 +168,8 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
return processor.VerifyResponse(upgrade, user, result);
}
- public void InvalidateUpgrade(HttpEntity entity)
- {
- SetUpgradeSecret(in entity.Session, null);
- }
+ public void InvalidateUpgrade(HttpEntity entity)
+ => SetUpgradeSecret(in entity.Session, null);
private MFAType[] GetEnbaledTypesForUser(IUser user)
{
@@ -206,7 +216,8 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
}
//Recover the upgrade message
- return doc.RootElement.GetProperty("upgrade").Deserialize<MfaChallenge>();
+ return doc.RootElement.GetProperty("upgrade")
+ .Deserialize<MfaChallenge>();
}
private void GetUpgradeMessage(MfaChallenge upgrade, ref string clientMessage, ref string secret)
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TOTPConfig.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TOTPConfig.cs
index eba54fe..938c310 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TOTPConfig.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TOTPConfig.cs
@@ -39,34 +39,34 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Totp
[JsonPropertyName("period_sec")]
public int PeriodSec
{
- get => (int)TOTPPeriod.TotalSeconds;
- set => TOTPPeriod = TimeSpan.FromSeconds(value);
+ get => (int)Period.TotalSeconds;
+ set => Period = TimeSpan.FromSeconds(value);
}
[JsonPropertyName("algorithm")]
public string AlgName
{
- get => TOTPAlg.ToString();
- set => TOTPAlg = Enum.Parse<HashAlg>(value.ToUpper(null));
+ get => HashAlg.ToString();
+ set => HashAlg = Enum.Parse<HashAlg>(value.ToUpper(null));
}
[JsonPropertyName("digits")]
- public int TOTPDigits { get; set; } = 6;
+ public int Digits { get; set; } = 6;
[JsonPropertyName("secret_size")]
- public int TOTPSecretBytes { get; set; } = 32;
+ public int SecretSize { get; set; } = 32;
[JsonPropertyName("window_size")]
- public int TOTPTimeWindowSteps { get; set; } = 1;
+ public int TimeWindowSteps { get; set; } = 1;
[JsonIgnore]
public bool Enabled => IssuerName != null;
[JsonIgnore]
- public HashAlg TOTPAlg { get; set; } = HashAlg.SHA1;
+ public HashAlg HashAlg { get; set; } = HashAlg.SHA1;
[JsonIgnore]
- public TimeSpan TOTPPeriod { get; set; } = TimeSpan.FromSeconds(30);
+ public TimeSpan Period { get; set; } = TimeSpan.FromSeconds(30);
internal static IValidator<TOTPConfig> GetValidator()
{
@@ -78,18 +78,20 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Totp
val.RuleFor(c => c.PeriodSec)
.InclusiveBetween(1, 600);
- val.RuleFor(c => c.TOTPAlg)
+ val.RuleFor(c => c.HashAlg)
.Must(a => a != HashAlg.None)
.WithMessage("TOTP Algorithim name must not be NONE");
- val.RuleFor(c => c.TOTPDigits)
+ val.RuleFor(c => c.Digits)
.GreaterThan(1)
- .WithMessage("You should have more than 1 digit for a totp code");
+ .WithMessage("You should have more than 1 digit for a totp code")
+ .LessThan(10)
+ .WithMessage("You should have less than 10 digits for a totp code");
//We dont neet to check window steps, the user may want to configure 0 or more
- val.RuleFor(c => c.TOTPTimeWindowSteps);
+ val.RuleFor(c => c.TimeWindowSteps);
- val.RuleFor(c => c.TOTPSecretBytes)
+ val.RuleFor(c => c.SecretSize)
.GreaterThan(8)
.WithMessage("You should configure a larger TOTP secret size for better security");
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TotpAuthProcessor.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TotpAuthProcessor.cs
index 393a745..ea29475 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TotpAuthProcessor.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/TotpAuthProcessor.cs
@@ -119,19 +119,19 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Totp
DateTimeOffset currentUtc = DateTimeOffset.UtcNow;
//Start the current window with the minimum window
- int currenStep = -config.TOTPTimeWindowSteps;
+ int currenStep = -config.TimeWindowSteps;
Span<byte> stepBuffer = stackalloc byte[sizeof(long)];
- Span<byte> hashBuffer = stackalloc byte[(int)config.TOTPAlg];
+ Span<byte> hashBuffer = stackalloc byte[(int)config.HashAlg];
//Run the loop at least once to allow a 0 step tight window
do
{
//Calculate the window by multiplying the window by the current step, then add it to the current time offset to produce a new window
- DateTimeOffset window = currentUtc.Add(config.TOTPPeriod.Multiply(currenStep));
+ DateTimeOffset window = currentUtc.Add(config.Period.Multiply(currenStep));
//calculate the time step
- long timeStep = (long)Math.Floor(window.ToUnixTimeSeconds() / config.TOTPPeriod.TotalSeconds);
+ long timeStep = (long)Math.Floor(window.ToUnixTimeSeconds() / config.Period.TotalSeconds);
//try to compute the hash, must always be storable in the buffer
bool writeResult = BitConverter.TryWriteBytes(stepBuffer, timeStep);
@@ -143,18 +143,18 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Totp
stepBuffer.Reverse();
}
- ERRNO result = ManagedHash.ComputeHmac(userSecret, stepBuffer, hashBuffer, config.TOTPAlg);
+ ERRNO result = ManagedHash.ComputeHmac(userSecret, stepBuffer, hashBuffer, config.HashAlg);
if (result < 1)
{
throw new InternalBufferTooSmallException("Failed to compute TOTP time step hash because the buffer was too small");
}
- codeMatches |= totpCode == CalcTOTPCode(config.TOTPDigits, hashBuffer[..(int)result]);
+ codeMatches |= totpCode == CalcTOTPCode(config.Digits, hashBuffer[..(int)result]);
currenStep++;
- } while (currenStep <= config.TOTPTimeWindowSteps);
+ } while (currenStep <= config.TimeWindowSteps);
return codeMatches;
}
@@ -182,5 +182,14 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Totp
public void ExtendUpgradePayload(in JwtPayload message, IUser user)
{ }
+
+ /// <summary>
+ /// Generates a new TOTP secret according to the system TOTP configuration
+ /// </summary>
+ /// <returns>The random secret of the configured size</returns>
+ public byte[] GenerateNewSecret()
+ {
+ return RandomHash.GetRandomBytes(config.SecretSize);
+ }
}
} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/UserTotpMfaExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/UserTotpMfaExtensions.cs
index c500a7e..b6b400e 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/UserTotpMfaExtensions.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/Totp/UserTotpMfaExtensions.cs
@@ -23,8 +23,8 @@
*/
using System;
+using System.Linq;
-using VNLib.Hashing;
using VNLib.Utils;
using VNLib.Plugins.Essentials.Users;
@@ -65,20 +65,66 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA.Totp
/// Generates/overwrites the current user's TOTP secret entry and returns a
/// byte array of the generated secret bytes
/// </summary>
- /// <param name="config">The system MFA configuration</param>
+ /// <param name="manager"></param>
+ /// <param name="user">The user to generate the secret for</param>
/// <returns>The raw secret that was encrypted and stored in the user's object</returns>
/// <exception cref="OutOfMemoryException"></exception>
- internal static byte[] MFAGenreateTOTPSecret(this IUser user, MFAConfig config)
+ internal static byte[]? TotpSetNewSecret(this MfaAuthManager manager, IUser user)
{
- _ = config.TOTPConfig ?? throw new NotSupportedException("The loaded configuration does not support TOTP");
- //Generate a random key
- byte[] newSecret = RandomHash.GetRandomBytes(config.TOTPConfig.TOTPSecretBytes);
- //Store secret in user storage
+ ArgumentNullException.ThrowIfNull(manager);
+ ArgumentNullException.ThrowIfNull(user);
+
+ //Get the totp processor if it exists
+ TotpAuthProcessor? proc = manager.Processors
+ .OfType<TotpAuthProcessor>()
+ .FirstOrDefault();
+
+ //May not be loaded to return null
+ if(proc is null)
+ {
+ return null;
+ }
+
+ byte[] newSecret = proc.GenerateNewSecret();
+
user.TotpSetSecret(VnEncoding.ToBase32String(newSecret, false));
- //return the raw secret bytes
+
return newSecret;
}
-
+ /// <summary>
+ /// Verifies a TOTP code for a given user instance.
+ /// </summary>
+ /// <param name="manager"></param>
+ /// <param name="user">The user to valid the code against</param>
+ /// <param name="code">The TOTP code to verify</param>
+ /// <returns>
+ /// True if totp is enabled and the code matches, false if the provider is no loaded, or the code does not match
+ /// </returns>
+ internal static bool TotpVerifyCode(this MfaAuthManager manager, IUser user, uint code)
+ {
+ ArgumentNullException.ThrowIfNull(manager);
+ ArgumentNullException.ThrowIfNull(user);
+
+ TotpAuthProcessor? proc = manager.Processors
+ .OfType<TotpAuthProcessor>()
+ .FirstOrDefault();
+
+ return proc is not null && proc.VerifyTOTP(user, code);
+ }
+
+ /// <summary>
+ /// Determines if TOTP is enabled for the plugin
+ /// </summary>
+ /// <param name="manager"></param>
+ /// <returns>True if the auth manager has a totp processor enabled</returns>
+ internal static bool TotpIsEnabled(this MfaAuthManager manager)
+ {
+ ArgumentNullException.ThrowIfNull(manager);
+
+ return manager.Processors
+ .Where(static p => p.Type == MFAType.TOTP)
+ .Any();
+ }
}
}