aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs23
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs8
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/StorageManager.cs2
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs38
-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
-rw-r--r--plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs26
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs3
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs2
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/stores/DbRouteStore.cs (renamed from plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/DbRouteStore.cs)17
-rw-r--r--plugins/VNLib.Plugins.Essentials.Content.Routing/src/stores/XmlRouteStore.cs (renamed from plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/XmlRouteStore.cs)5
23 files changed, 267 insertions, 287 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs
index b95930d..3c4f3e5 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Endpoints/WebEndpoint.cs
@@ -33,31 +33,24 @@ using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Essentials.Extensions;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Validation;
+using VNLib.Plugins.Extensions.Loading.Routing;
using VNLib.Plugins.Essentials.Accounts.AppData.Model;
using VNLib.Plugins.Essentials.Accounts.AppData.Stores;
namespace VNLib.Plugins.Essentials.Accounts.AppData.Endpoints
{
+
+ [EndpointPath("{{path}}")]
+ [EndpointLogName("Endpoint")]
[ConfigurationName("web_endpoint")]
- internal sealed class WebEndpoint : ProtectedWebEndpoint
+ internal sealed class WebEndpoint(PluginBase plugin, IConfigScope config) : ProtectedWebEndpoint
{
const int DefaultMaxDataSize = 8 * 1024;
- private readonly StorageManager _store;
- private readonly int MaxDataSize;
- private readonly string[] AllowedScopes;
-
- public WebEndpoint(PluginBase plugin, IConfigScope config)
- {
- string path = config.GetRequiredProperty<string>("path");
- InitPathAndLog(path, plugin.Log.CreateScope("Endpoint"));
-
- MaxDataSize = config.GetValueOrDefault("max_data_size", DefaultMaxDataSize);
- AllowedScopes = config.GetRequiredProperty<string[]>("allowed_scopes");
-
- _store = plugin.GetOrCreateSingleton<StorageManager>();
- }
+ private readonly StorageManager _store = plugin.GetOrCreateSingleton<StorageManager>();
+ private readonly int MaxDataSize = config.GetValueOrDefault("max_data_size", DefaultMaxDataSize);
+ private readonly string[] AllowedScopes = config.GetRequiredProperty<string[]>("allowed_scopes");
protected async override ValueTask<VfReturnType> GetAsync(HttpEntity entity)
{
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs
index 0281a0b..2347c66 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/Sql/SqlBackingStore.cs
@@ -80,10 +80,10 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Stores.Sql
{
return _store.AddOrUpdateAsync(new DataRecord
{
- UserId = request.UserId,
- RecordKey = request.RecordKey,
- Data = entity.Data,
- Checksum = entity.Checksum.HasValue ? unchecked((long)entity.Checksum.Value) : 0,
+ UserId = request.UserId,
+ RecordKey = request.RecordKey,
+ Data = entity.Data,
+ Checksum = entity.Checksum.HasValue ? unchecked((long)entity.Checksum.Value) : 0,
}, cancellation);
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/StorageManager.cs b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/StorageManager.cs
index bbfe9c6..3cd8edb 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/StorageManager.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.AppData/src/Stores/StorageManager.cs
@@ -69,7 +69,7 @@ namespace VNLib.Plugins.Essentials.Accounts.AppData.Stores
if(cConfig is null || cConfig.Enabled == false)
{
- _logger.Debug("Result cache disabled via configuration, or not set");
+ _logger.Information("Result cache disabled via configuration, or not set");
return;
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
index 0c8ded2..c1f756d 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
@@ -49,6 +49,7 @@ using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Sql;
using VNLib.Plugins.Extensions.Loading.Events;
using VNLib.Plugins.Extensions.Loading.Users;
+using VNLib.Plugins.Extensions.Loading.Routing;
using VNLib.Plugins.Extensions.Validation;
using VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation;
using static VNLib.Plugins.Essentials.Accounts.AccountUtil;
@@ -56,8 +57,14 @@ using static VNLib.Plugins.Essentials.Accounts.AccountUtil;
namespace VNLib.Plugins.Essentials.Accounts.Registration.Endpoints
{
+ /// <summary>
+ /// Creates back-end functionality for a "registration" or "sign-up" page that integrates with the <see cref="AccountUtil"/> plugin
+ /// </summary>
+ /// <param name="Path">The path identifier</param>
+ /// <exception cref="ArgumentException"></exception>
+ [EndpointPath("{{path}}")]
[ConfigurationName("registration")]
- internal sealed class RegistrationEntpoint : UnprotectedWebEndpoint
+ internal sealed class RegistrationEntpoint(PluginBase plugin, IConfigScope config) : UnprotectedWebEndpoint
{
/// <summary>
/// Generates a CNG random buffer to use as a nonce
@@ -69,33 +76,14 @@ namespace VNLib.Plugins.Essentials.Accounts.Registration.Endpoints
private static readonly IValidator<RegCompletionRequest> RegCompletionValidator = RegCompletionRequest.GetValidator();
- private readonly IUserManager Users;
- private readonly RevokedTokenStore RevokedTokens;
- private readonly TransactionalEmailConfig Emails;
- private readonly IAsyncLazy<ReadOnlyJsonWebKey> RegSignatureKey;
- private readonly TimeSpan RegExpiresSec;
- /// <summary>
- /// Creates back-end functionality for a "registration" or "sign-up" page that integrates with the <see cref="AccountUtil"/> plugin
- /// </summary>
- /// <param name="Path">The path identifier</param>
- /// <exception cref="ArgumentException"></exception>
- public RegistrationEntpoint(PluginBase plugin, IConfigScope config)
- {
- string? path = config["path"].GetString();
+ private readonly TimeSpan RegExpiresSec = config.GetRequiredProperty("reg_expires_sec", p => p.GetTimeSpan(TimeParseType.Seconds));
+ private readonly IUserManager Users = plugin.GetOrCreateSingleton<UserManager>();
+ private readonly RevokedTokenStore RevokedTokens = new(plugin.GetContextOptionsAsync());
+ private readonly TransactionalEmailConfig Emails = plugin.GetOrCreateSingleton<TEmailConfig>();
- InitPathAndLog(path, plugin.Log);
-
- RegExpiresSec = config["reg_expires_sec"].GetTimeSpan(TimeParseType.Seconds);
-
- Users = plugin.GetOrCreateSingleton<UserManager>();
- Emails = plugin.GetOrCreateSingleton<TEmailConfig>();
- RevokedTokens = new(plugin.GetContextOptionsAsync());
-
- //Begin the async op to get the signature key from the vault
- RegSignatureKey = plugin.GetSecretAsync("reg_sig_key")
+ private readonly IAsyncLazy<ReadOnlyJsonWebKey> RegSignatureKey = plugin.GetSecretAsync("reg_sig_key")
.ToLazy(static sr => sr.GetJsonWebKey());
- }
//Schedule cleanup interval
[AsyncInterval(Minutes = 5)]
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();
+ }
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs
index 177988f..4e31a46 100644
--- a/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Auth.Social/src/PortalsEndpoint.cs
@@ -23,6 +23,7 @@
*/
using System;
+using System.IO;
using System.Net;
using System.Linq;
using System.Text.Json;
@@ -32,36 +33,33 @@ using VNLib.Utils.IO;
using VNLib.Net.Http;
using VNLib.Plugins.Essentials.Endpoints;
using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Routing;
namespace VNLib.Plugins.Essentials.Auth.Social
{
+
+ [EndpointPath("{{path}}")]
[ConfigurationName("portals")]
- internal sealed class PortalsEndpoint : UnprotectedWebEndpoint, IDisposable
+ internal sealed class PortalsEndpoint() : UnprotectedWebEndpoint, IDisposable
{
- private readonly VnMemoryStream _portals;
-
- public PortalsEndpoint(PluginBase plugin, IConfigScope config)
- {
- string path = config.GetRequiredProperty("path", p => p.GetString()!);
- InitPathAndLog(path, plugin.Log);
-
- _portals = new VnMemoryStream();
- }
+ private readonly VnMemoryStream _portals = new();
public void SetPortals(IEnumerable<SocialOAuthPortal> portals)
{
//Convert to json
PortalDefJson[] jsn = portals.Select(p => new PortalDefJson
{
- id = p.PortalId,
- login = p.LoginEndpoint.Path,
- logout = p.LogoutEndpoint?.Path,
- icon = p.Base64Icon
+ id = p.PortalId,
+ login = p.LoginEndpoint.Path,
+ logout = p.LogoutEndpoint?.Path,
+ icon = p.Base64Icon
}).ToArray();
//Serialize portals array to memory stream
JsonSerializer.Serialize(_portals, jsn);
+ _portals.Seek(0, SeekOrigin.Begin);
+
//Set memory stream to readonly so shallow copy can be returned
_ = VnMemoryStream.CreateReadonly(_portals);
}
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs
index e2b87a1..8c32e71 100644
--- a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/ManagedRouteStore.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Content.Routing
@@ -29,6 +29,7 @@ using System.Collections.Generic;
using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Sql;
using VNLib.Plugins.Essentials.Content.Routing.Model;
+using VNLib.Plugins.Essentials.Content.Routing.stores;
namespace VNLib.Plugins.Essentials.Content.Routing
{
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs
index 72f091a..d89ef37 100644
--- a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Router.cs
@@ -45,7 +45,7 @@ namespace VNLib.Plugins.Essentials.Content.Routing
{
private static readonly RouteComparer Comparer = new();
- private readonly IRouteStore Store = plugin.GetOrCreateSingleton<ManagedRouteStore>();
+ private readonly ManagedRouteStore Store = plugin.GetOrCreateSingleton<ManagedRouteStore>();
private readonly ILogProvider Logger = plugin.Log;
private readonly ConcurrentDictionary<IWebProcessor, Task<ReadOnlyCollection<Route>>> RouteTable = new();
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/DbRouteStore.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/stores/DbRouteStore.cs
index b7f64e2..634511b 100644
--- a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/DbRouteStore.cs
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/stores/DbRouteStore.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2023 Vaughn Nugent
+* Copyright (c) 2024 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Content.Routing
@@ -35,21 +35,16 @@ using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Sql;
using VNLib.Plugins.Extensions.Data.Abstractions;
using VNLib.Plugins.Extensions.Data.Extensions;
+using VNLib.Plugins.Essentials.Content.Routing.Model;
-namespace VNLib.Plugins.Essentials.Content.Routing.Model
+namespace VNLib.Plugins.Essentials.Content.Routing.stores
{
- internal sealed class DbRouteStore : DbStore<Route>, IRouteStore
+ internal sealed class DbRouteStore(PluginBase plugin) : DbStore<Route>, IRouteStore
{
- private readonly IAsyncLazy<DbContextOptions> Options;
+ private readonly IAsyncLazy<DbContextOptions> Options = plugin.GetContextOptionsAsync();
public override IDbQueryLookup<Route> QueryTable { get; } = new DbQueries();
- public DbRouteStore(PluginBase plugin)
- {
- //Load the db context options
- Options = plugin.GetContextOptionsAsync();
- }
-
///<inheritdoc/>
public Task GetAllRoutesAsync(ICollection<Route> routes, CancellationToken cancellation)
{
@@ -69,7 +64,7 @@ namespace VNLib.Plugins.Essentials.Content.Routing.Model
throw new NotSupportedException();
}
- private sealed record class DbQueries : IDbQueryLookup<Route>
+ private sealed class DbQueries : IDbQueryLookup<Route>
{
public IQueryable<Route> GetCollectionQueryBuilder(IDbContextHandle context, params string[] constraints)
{
diff --git a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/XmlRouteStore.cs b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/stores/XmlRouteStore.cs
index fce193e..52e52e4 100644
--- a/plugins/VNLib.Plugins.Essentials.Content.Routing/src/Model/XmlRouteStore.cs
+++ b/plugins/VNLib.Plugins.Essentials.Content.Routing/src/stores/XmlRouteStore.cs
@@ -34,8 +34,9 @@ using VNLib.Utils.Memory;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Essentials.Content.Routing.Model;
-namespace VNLib.Plugins.Essentials.Content.Routing.Model
+namespace VNLib.Plugins.Essentials.Content.Routing.stores
{
[ConfigurationName("store")]
internal sealed class XmlRouteStore : IRouteStore
@@ -45,7 +46,7 @@ namespace VNLib.Plugins.Essentials.Content.Routing.Model
public XmlRouteStore(PluginBase plugin, IConfigScope config)
{
//Get the route file path
- _routeFile = config["route_file"].GetString() ?? throw new KeyNotFoundException("Missing required key 'route_file' in 'route_store' configuration element");
+ _routeFile = config.GetRequiredProperty<string>("route_file");
//Make sure the file exists
if (!FileOperations.FileExists(_routeFile))