diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts')
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(); + } } } |