aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs135
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs4
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAUpgrade.cs15
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs50
4 files changed, 107 insertions, 97 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs
index 26a853a..5b50cb2 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/LoginEndpoint.cs
@@ -31,6 +31,7 @@ using System.Text.Json.Serialization;
using FluentValidation;
+using VNLib.Utils;
using VNLib.Utils.Memory;
using VNLib.Utils.Logging;
using VNLib.Utils.Extensions;
@@ -44,6 +45,17 @@ using VNLib.Plugins.Extensions.Loading;
using VNLib.Plugins.Extensions.Loading.Users;
using static VNLib.Plugins.Essentials.Statics;
+/*
+ * Password only log-ins should be immune to repeat attacks on the same backend, because sessions are
+ * guarunteed to be mutally exclusive on the same system, therefor a successful login cannot be repeated
+ * without a logout with the proper authorization.
+ *
+ * Since MFA upgrades are indempodent upgrades can be regenerated continually as long as the session
+ * is not authorized, however login authorizations should be immune to repeats because session locking
+ *
+ * Session id's are also regenerated per request, the only possible vector could be stale session cache
+ * that has a valid MFA key and an old, but valid session id.
+ */
namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
@@ -61,7 +73,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
private static readonly LoginMessageValidation LmValidator = new();
private readonly IPasswordHashingProvider Passwords;
- private readonly MFAConfig? MultiFactor;
+ private readonly MFAConfig MultiFactor;
private readonly IUserManager Users;
private readonly uint MaxFailedLogins;
private readonly TimeSpan FailedCountTimeout;
@@ -79,16 +91,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
MultiFactor = pbase.GetConfigElement<MFAConfig>();
}
- private class MfaUpgradeWebm : ValErrWebMessage
+ protected override ERRNO PreProccess(HttpEntity entity)
{
- [JsonPropertyName("pwtoken")]
- public string? PasswordToken { get; set; }
-
- [JsonPropertyName("mfa")]
- public bool? MultiFactorUpgrade { get; set; } = null;
+ //Cannot have new sessions
+ return base.PreProccess(entity) && !entity.Session.IsNew;
}
-
protected async override ValueTask<VfReturnType> PostAsync(HttpEntity entity)
{
//Conflict if user is logged in
@@ -99,7 +107,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
}
//If mfa is enabled, allow processing via mfa
- if (MultiFactor != null)
+ if (MultiFactor.FIDOEnabled || MultiFactor.TOTPEnabled)
{
if (entity.QueryArgs.ContainsKey("mfa"))
{
@@ -109,7 +117,6 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
return await ProccesLoginAsync(entity);
}
-
private async ValueTask<VfReturnType> ProccesLoginAsync(HttpEntity entity)
{
MfaUpgradeWebm webm = new();
@@ -173,13 +180,28 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
Log.Warn(uue);
return VfReturnType.Error;
}
- }
-
+ }
+
private bool LoginUser(HttpEntity entity, LoginMessage loginMessage, IUser user, MfaUpgradeWebm webm)
{
//Verify password before we tell the user the status of their account for security reasons
- if (!Passwords.Verify(user.PassHash, new PrivateString(loginMessage.Password, false)))
+ if (!Passwords.Verify(user.PassHash, loginMessage.Password))
+ {
+ return false;
+ }
+
+ //Only allow active users
+ if (user.Status != UserStatus.Active)
+ {
+ //This is an unhandled case, and should never happen, but just incase write a warning to the log
+ Log.Warn("Account {uid} has invalid status key and a login was attempted from {ip}", user.UserID, entity.TrustedRemoteIp);
+ return false;
+ }
+
+ //Is the account restricted to a local network connection?
+ if (user.LocalOnly && !entity.IsLocalConnection)
{
+ Log.Information("User {uid} attempted a login from a non-local network with the correct password. Access was denied", user.UserID);
return false;
}
@@ -188,34 +210,27 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
try
{
- if (user.Status == UserStatus.Active)
- {
- //Is the account restricted to a local network connection?
- if (user.LocalOnly && !entity.IsLocalConnection)
- {
- Log.Information("User {uid} attempted a login from a non-local network with the correct password. Access was denied", user.UserID);
- return false;
- }
+ //get the new upgrade jwt string
+ MfaUpgradeMessage? message = user.MFAGetUpgradeIfEnabled(MultiFactor, loginMessage);
- //get the new upgrade jwt string
- Tuple<string, string>? message = user.MFAGetUpgradeIfEnabled(MultiFactor, loginMessage);
+ /*
+ * Mfa is essentially indempodent, the session stores the last upgrade key, so
+ * if this method is continually called, new mfa tokens will be generated.
+ */
- //if message is null, mfa was not enabled or could not be prepared
- if (message != null)
- {
- //Store the base64 signature
- entity.Session.MfaUpgradeSecret(message.Item2);
-
- //send challenge message to client
- webm.Result = message.Item1;
- webm.Success = true;
- webm.MultiFactorUpgrade = true;
-
- return true;
- }
+ //if message is null, mfa was not enabled or could not be prepared
+ if (message.HasValue)
+ {
+ //Store the base64 signature
+ entity.Session.MfaUpgradeSecret(message.Value.SessionKey);
- //Set password token
- webm.PasswordToken = null;
+ //send challenge message to client
+ webm.Result = message.Value.ClientJwt;
+ webm.MultiFactorUpgrade = true;
+ }
+ else
+ {
+ /* SUCCESSFULL LOGIN! */
//Elevate the login status of the session to reflect the user's status
entity.GenerateAuthorization(loginMessage, user, webm);
@@ -226,18 +241,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
EmailAddress = user.EmailAddress,
};
- webm.Success = true;
//Write to log
Log.Verbose("Successful login for user {uid}...", user.UserID[..8]);
-
- return true;
- }
- else
- {
- //This is an unhandled case, and should never happen, but just incase write a warning to the log
- Log.Warn("Account {uid} has invalid status key and a login was attempted from {ip}", user.UserID, entity.TrustedRemoteIp);
- return false;
}
+
+ webm.Success = true;
+ return true;
}
/*
* Account auhorization may throw excetpions if the configuration does not
@@ -254,8 +263,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
Log.Debug(ce);
}
return false;
- }
-
+ }
private async ValueTask<VfReturnType> ProcessMfaAsync(HttpEntity entity)
{
@@ -340,10 +348,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
//get totp code from request
uint code = request.RootElement.GetProperty("code").GetUInt32();
+
//Verify totp code
if (!MultiFactor!.VerifyTOTP(user, code))
{
webm.Result = "Please check your code.";
+
//Increment flc and update the user in the store
user.FailedLoginIncrement(entity.RequestedTimeUtc);
return;
@@ -361,30 +371,22 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
return;
}
+ //SUCCESSFUL LOGIN
+
//Wipe session signature
entity.Session.MfaUpgradeSecret(null);
- //build login message from upgrade
- LoginMessage loginMessage = new()
- {
- ClientId = upgrade.ClientID,
- ClientPublicKey = upgrade.Base64PubKey,
- LocalLanguage = upgrade.ClientLocalLanguage,
- LocalTime = localTime,
- UserName = upgrade.UserName
- };
-
//Elevate the login status of the session to reflect the user's status
- entity.GenerateAuthorization(loginMessage, user, webm);
-
- //Set the password token as the password field of the login message
- webm.PasswordToken = upgrade.PwClientData;
+ entity.GenerateAuthorization(upgrade, user, webm);
+
//Send the Username (since they already have it)
webm.Result = new AccountData()
{
EmailAddress = user.EmailAddress,
};
+
webm.Success = true;
+
//Write to log
Log.Verbose("Successful login for user {uid}...", user.UserID[..8]);
}
@@ -411,5 +413,12 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
//Count has been exceeded, and has not timed out yet
return true;
}
+
+ private sealed class MfaUpgradeWebm : ValErrWebMessage
+ {
+
+ [JsonPropertyName("mfa")]
+ public bool? MultiFactorUpgrade { get; set; } = null;
+ }
}
} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs
index e7c8a86..48a3345 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PkiLoginEndpoint.cs
@@ -54,6 +54,10 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints
{
public const string INVALID_MESSAGE = "Your assertion is invalid, please regenerate and try again";
+ /*
+ * I am only supporting EC keys for size reasons, user objects are limited in back-end size and keys can
+ * take up large ammounts of data.
+ */
private static readonly ImmutableArray<string> AllowedCurves = new string[3] { "P-256", "P-384", "P-521"}.ToImmutableArray();
private static readonly ImmutableArray<string> AllowedAlgs = new string[3] { "ES256", "ES384", "ES512" }.ToImmutableArray();
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAUpgrade.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAUpgrade.cs
index 5577d51..e69088a 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAUpgrade.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/MFAUpgrade.cs
@@ -1,5 +1,5 @@
/*
-* Copyright (c) 2022 Vaughn Nugent
+* Copyright (c) 2023 Vaughn Nugent
*
* Library: VNLib
* Package: VNLib.Plugins.Essentials.Accounts
@@ -24,17 +24,15 @@
using System.Text.Json.Serialization;
-#nullable enable
-
namespace VNLib.Plugins.Essentials.Accounts.MFA
{
- internal class MFAUpgrade
+ internal class MFAUpgrade : IClientSecInfo
{
/// <summary>
/// The login's client id specifier
/// </summary>
[JsonPropertyName("cid")]
- public string? ClientID { get; set; }
+ public string? ClientId { get; set; }
/// <summary>
/// The id of the user that is requesting a login
/// </summary>
@@ -50,16 +48,11 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// public key
/// </summary>
[JsonPropertyName("pubkey")]
- public string? Base64PubKey { get; set; }
+ public string? PublicKey { get; set; }
/// <summary>
/// The user's specified language
/// </summary>
[JsonPropertyName("lang")]
public string? ClientLocalLanguage { get; set; }
- /// <summary>
- /// The encrypted password token for the client
- /// </summary>
- [JsonPropertyName("cd")]
- public string? PwClientData { get; set; }
}
}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
index 63b2a2b..99f7fbb 100644
--- a/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
+++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/MFA/UserMFAExtensions.cs
@@ -23,12 +23,10 @@
*/
using System;
-using System.Text;
using System.Linq;
using System.Buffers;
using System.Text.Json;
using System.Collections.Generic;
-using System.Security.Cryptography;
using System.Text.Json.Serialization;
using VNLib.Hashing;
@@ -229,10 +227,16 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
return jwt.VerifyFromJwk(jwk);
}
- public static void PKISetUserKey(this IUser user, IReadOnlyDictionary<string, string>? keyData)
+ public static void PKISetUserKey(this IUser user, IReadOnlyDictionary<string, string>? keyFields)
{
+ //Serialize the key data
+ byte[] keyData = JsonSerializer.SerializeToUtf8Bytes(keyFields, Statics.SR_OPTIONS);
+
+ //convert to base32 string before writing user data
+ string base64 = Convert.ToBase64String(keyData);
+
//Store key data
- user.SetObject(USER_PKI_ENTRY, keyData);
+ user[USER_PKI_ENTRY] = base64;
}
private static ReadOnlyJsonWebKey? RecoverKey(IUser user)
@@ -248,9 +252,14 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
byte[] buffer = ArrayPool<byte>.Shared.Rent(JWK_KEY_BUFFER_SIZE);
try
{
- //Recover bytes and get the jwk from the data
- int encoded = Encoding.UTF8.GetBytes(keyData, buffer);
- return new ReadOnlyJsonWebKey(buffer.AsSpan(0, encoded));
+ //Recover base64 bytes from key data
+ ERRNO bytes = VnEncoding.TryFromBase64Chars(keyData, buffer);
+ if (!bytes)
+ {
+ return null;
+ }
+ //Recover json from the decoded binary data
+ return new ReadOnlyJsonWebKey(buffer.AsSpan(0, bytes));
}
finally
{
@@ -293,7 +302,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
#endregion
- private static HMAC GetSigningAlg(byte[] key) => new HMACSHA256(key);
+ private static HashAlg SigingAlg { get; } = HashAlg.SHA256;
private static ReadOnlyMemory<byte> UpgradeHeader { get; } = CompileJwtHeader();
@@ -313,7 +322,6 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// </summary>
/// <param name="config"></param>
/// <param name="upgradeJwtString">The signed JWT upgrade message</param>
- /// <param name="upgrade">The recovered upgrade</param>
/// <param name="base32Secret">The stored base64 encoded signature from the session that requested an upgrade</param>
/// <returns>True if the upgrade was verified, not expired, and was recovered from the signed message, false otherwise</returns>
public static MFAUpgrade? RecoverUpgrade(this MFAConfig config, string upgradeJwtString, string base32Secret)
@@ -325,10 +333,8 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
byte[] secret = VnEncoding.FromBase32String(base32Secret)!;
try
{
- //Verify the
- using HMAC hmac = GetSigningAlg(secret);
-
- if (!jwt.Verify(hmac))
+ //Verify the signature
+ if (!jwt.Verify(secret, SigingAlg))
{
return null;
}
@@ -364,7 +370,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
/// <param name="login">The message from the user requesting the login</param>
/// <returns>A signed upgrade message the client will pass back to the server after the MFA verification</returns>
/// <exception cref="InvalidOperationException"></exception>
- public static Tuple<string, string>? MFAGetUpgradeIfEnabled(this IUser user, MFAConfig? conf, LoginMessage login)
+ public static MfaUpgradeMessage? MFAGetUpgradeIfEnabled(this IUser user, MFAConfig? conf, LoginMessage login)
{
//Webauthn config
@@ -383,8 +389,8 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
Type = MFAType.TOTP,
//Store login message details
UserName = login.UserName,
- ClientID = login.ClientId,
- Base64PubKey = login.ClientPublicKey,
+ ClientId = login.ClientId,
+ PublicKey = login.ClientPublicKey,
ClientLocalLanguage = login.LocalLanguage,
};
@@ -394,7 +400,7 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
return null;
}
- private static Tuple<string, string> GetUpgradeMessage(MFAUpgrade upgrade, MFAConfig config)
+ private static MfaUpgradeMessage GetUpgradeMessage(MFAUpgrade upgrade, MFAConfig config)
{
//Add some random entropy to the upgrade message, to help prevent forgery
string entropy = RandomHash.GetRandomBase32(config.NonceLenBytes);
@@ -414,12 +420,8 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
//Generate a new random secret
byte[] secret = RandomHash.GetRandomBytes(config.UpgradeKeyBytes);
- //Init alg
- using(HMAC alg = GetSigningAlg(secret))
- {
- //sign jwt
- upgradeJwt.Sign(alg);
- }
+ //sign jwt
+ upgradeJwt.Sign(secret, SigingAlg);
//compile and return jwt upgrade
return new(upgradeJwt.Compile(), VnEncoding.ToBase32String(secret));
@@ -429,4 +431,6 @@ namespace VNLib.Plugins.Essentials.Accounts.MFA
public static string? MfaUpgradeSecret(this in SessionInfo session) => session[SESSION_SIG_KEY];
}
+
+ readonly record struct MfaUpgradeMessage(string ClientJwt, string SessionKey);
}