diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs')
-rw-r--r-- | plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs | 97 |
1 files changed, 64 insertions, 33 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs index df20084..0b015a4 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/MFAEndpoint.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2022 Vaughn Nugent +* Copyright (c) 2023 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Accounts @@ -29,7 +29,6 @@ using System.Threading.Tasks; using System.Collections.Generic; using System.Text.Json.Serialization; -using VNLib.Hashing; using VNLib.Utils; using VNLib.Utils.Memory; using VNLib.Utils.Logging; @@ -51,14 +50,16 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints private readonly IUserManager Users; private readonly MFAConfig? MultiFactor; + private readonly IPasswordHashingProvider Passwords; - public MFAEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) + public MFAEndpoint(PluginBase pbase, IConfigScope config) { string? path = config["path"].GetString(); InitPathAndLog(path, pbase.Log); - Users = pbase.GetUserManager(); - MultiFactor = pbase.GetMfaConfig(); + Users = pbase.GetOrCreateSingleton<UserManager>(); + MultiFactor = pbase.GetConfigElement<MFAConfig>(); + Passwords = pbase.GetPasswords(); } private class TOTPUpdateMessage @@ -78,18 +79,22 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) { List<string> enabledModes = new(2); + //Load the MFA entry for the user using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); + //Set the TOTP flag if set if (!string.IsNullOrWhiteSpace(user?.MFAGetTOTPSecret())) { enabledModes.Add("totp"); } + //TODO Set fido flag if enabled if (!string.IsNullOrWhiteSpace("")) { enabledModes.Add("fido"); } + //Return mfa modes as an array entity.CloseResponseJson(HttpStatusCode.OK, enabledModes); return VfReturnType.VirtualSkip; @@ -101,6 +106,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints //Get the request message using JsonDocument? mfaRequest = await entity.GetJsonFromFileAsync(); + if (webm.Assert(mfaRequest != null, "Invalid request")) { entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); @@ -130,8 +136,17 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints return VfReturnType.VirtualSkip; } + //Get the user entry + using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); + + if (webm.Assert(user != null, "Please log-out and try again.")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + //get the user's password challenge - using (PrivateString? password = (PrivateString?)mfaRequest.RootElement.GetPropString("challenge")) + using (PrivateString? password = (PrivateString?)mfaRequest.RootElement.GetPropString("password")) { if (PrivateString.IsNullOrEmpty(password)) { @@ -139,26 +154,28 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm); return VfReturnType.VirtualSkip; } - //Verify challenge - if (!entity.Session.VerifyChallenge(password)) + + //Verify password against the user + if (!user.VerifyPassword(password, Passwords)) { webm.Result = "Please check your password"; entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm); return VfReturnType.VirtualSkip; } } - //Get the user entry - using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); - if (webm.Assert(user != null, "Please log-out and try again.")) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } + switch (mfaType.ToLower()) { //Process a Time based one time password(TOTP) creation/regeneration case "totp": { + //Confirm totp is enabled + if (webm.Assert(MultiFactor.TOTPEnabled, "TOTP is not enabled on the current server")) + { + entity.CloseResponse(webm); + return VfReturnType.VirtualSkip; + } + //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); //Alloc output buffer @@ -167,7 +184,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints try { //Encrypt the secret for the client - ERRNO count = entity.Session.TryEncryptClientData(secretBuffer, outputBuffer.Span); + ERRNO count = entity.TryEncryptClientData(secretBuffer, outputBuffer.Span); if (!count) { @@ -179,10 +196,10 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints webm.Result = new TOTPUpdateMessage() { - Issuer = MultiFactor.IssuerName, - Digits = MultiFactor.TOTPDigits, - Period = (int)MultiFactor.TOTPPeriod.TotalSeconds, - Algorithm = MultiFactor.TOTPAlg.ToString(), + Issuer = MultiFactor.TOTPConfig.IssuerName, + Digits = MultiFactor.TOTPConfig.TOTPDigits, + Period = (int)MultiFactor.TOTPConfig.TOTPPeriod.TotalSeconds, + Algorithm = MultiFactor.TOTPConfig.TOTPAlg.ToString(), //Convert the secret to base64 string to send to client Base64EncSecret = Convert.ToBase64String(outputBuffer.Span[..(int)count]) }; @@ -194,7 +211,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints { //dispose the output buffer outputBuffer.Dispose(); - RandomHash.GetRandomBytes(secretBuffer); + MemoryUtil.InitializeBlock(secretBuffer.AsSpan()); } //Only write changes to the db of operation was successful await user.ReleaseAsync(); @@ -229,25 +246,38 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); return VfReturnType.VirtualSkip; } - /* - * An MFA upgrade requires a challenge to be verified because - * it can break the user's ability to access their account - */ - string? challenge = request.RootElement.GetProperty("challenge").GetString(); + string? mfaType = request.RootElement.GetProperty("type").GetString(); - if (!entity.Session.VerifyChallenge(challenge)) - { - webm.Result = "Please check your password"; - //return unauthorized - entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm); - return VfReturnType.VirtualSkip; - } + //get the user using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); if (user == null) { return VfReturnType.NotFound; } + + /* + * An MFA upgrade requires a challenge to be verified because + * it can break the user's ability to access their account + */ + using (PrivateString? password = (PrivateString?)request.RootElement.GetPropString("password")) + { + if (PrivateString.IsNullOrEmpty(password)) + { + webm.Result = "Please check your password"; + entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm); + return VfReturnType.VirtualSkip; + } + + //Verify password against the user + if (!user.VerifyPassword(password, Passwords)) + { + webm.Result = "Please check your password"; + entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm); + return VfReturnType.VirtualSkip; + } + } + //Check for totp disable if ("totp".Equals(mfaType, StringComparison.OrdinalIgnoreCase)) { @@ -271,6 +301,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints { webm.Result = "Invalid MFA type"; } + //Must write response while password is in scope entity.CloseResponse(webm); return VfReturnType.VirtualSkip; |