diff options
Diffstat (limited to 'VNLib.Plugins.Essentials.Accounts/Endpoints')
6 files changed, 0 insertions, 1081 deletions
diff --git a/VNLib.Plugins.Essentials.Accounts/Endpoints/KeepAliveEndpoint.cs b/VNLib.Plugins.Essentials.Accounts/Endpoints/KeepAliveEndpoint.cs deleted file mode 100644 index fe5a65b..0000000 --- a/VNLib.Plugins.Essentials.Accounts/Endpoints/KeepAliveEndpoint.cs +++ /dev/null @@ -1,64 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Accounts -* File: KeepAliveEndpoint.cs -* -* KeepAliveEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Text.Json; -using System.Collections.Generic; - -using VNLib.Plugins.Essentials.Endpoints; -using VNLib.Plugins.Extensions.Loading; - -namespace VNLib.Plugins.Essentials.Accounts.Endpoints -{ - [ConfigurationName("keepalive_endpoint")] - internal sealed class KeepAliveEndpoint : ProtectedWebEndpoint - { - /* - * Endpoint does not use a log, so IniPathAndLog is never called - * and path verification happens verbosly - */ - public KeepAliveEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) - { - string? path = config["path"].GetString(); - - InitPathAndLog(path, pbase.Log); - } - - protected override VfReturnType Get(HttpEntity entity) - { - //Return okay - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; - } - - //Allow post to update user's credentials - protected override VfReturnType Post(HttpEntity entity) - { - //Return okay - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; - } - } -}
\ No newline at end of file diff --git a/VNLib.Plugins.Essentials.Accounts/Endpoints/LoginEndpoint.cs b/VNLib.Plugins.Essentials.Accounts/Endpoints/LoginEndpoint.cs deleted file mode 100644 index 4100620..0000000 --- a/VNLib.Plugins.Essentials.Accounts/Endpoints/LoginEndpoint.cs +++ /dev/null @@ -1,410 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Accounts -* File: LoginEndpoint.cs -* -* LoginEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; -using System.Security.Cryptography; -using System.Text.Json.Serialization; - -using VNLib.Hashing; -using VNLib.Utils; -using VNLib.Utils.Memory; -using VNLib.Utils.Logging; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Users; -using VNLib.Plugins.Essentials.Endpoints; -using VNLib.Plugins.Essentials.Extensions; -using VNLib.Plugins.Extensions.Validation; -using VNLib.Plugins.Essentials.Accounts.MFA; -using VNLib.Plugins.Essentials.Accounts.Validators; -using VNLib.Plugins.Extensions.Loading; -using VNLib.Plugins.Extensions.Loading.Users; -using static VNLib.Plugins.Essentials.Statics; -using static VNLib.Plugins.Essentials.Accounts.AccountManager; - - -namespace VNLib.Plugins.Essentials.Accounts.Endpoints -{ - - /// <summary> - /// Provides an authentication endpoint for user-accounts - /// </summary> - [ConfigurationName("login_endpoint")] - internal sealed class LoginEndpoint : UnprotectedWebEndpoint - { - public const string INVALID_MESSAGE = "Please check your email or password."; - public const string LOCKED_ACCOUNT_MESSAGE = "You have been timed out, please try again later"; - public const string MFA_ERROR_MESSAGE = "Invalid or expired request."; - - private static readonly LoginMessageValidation LmValidator = new(); - - private readonly PasswordHashing Passwords; - private readonly MFAConfig? MultiFactor; - private readonly IUserManager Users; - private readonly uint MaxFailedLogins; - private readonly TimeSpan FailedCountTimeout; - - public LoginEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) - { - string? path = config["path"].GetString(); - FailedCountTimeout = config["failed_count_timeout_sec"].GetTimeSpan(TimeParseType.Seconds); - MaxFailedLogins = config["failed_count_max"].GetUInt32(); - - InitPathAndLog(path, pbase.Log); - - Passwords = pbase.GetPasswords(); - Users = pbase.GetUserManager(); - MultiFactor = pbase.GetMfaConfig(); - } - - private class MfaUpgradeWebm : ValErrWebMessage - { - [JsonPropertyName("pwtoken")] - public string? PasswordToken { get; set; } - - [JsonPropertyName("mfa")] - public bool? MultiFactorUpgrade { get; set; } = null; - } - - - protected async override ValueTask<VfReturnType> PostAsync(HttpEntity entity) - { - //Conflict if user is logged in - if (entity.LoginCookieMatches() || entity.TokenMatches()) - { - entity.CloseResponse(HttpStatusCode.Conflict); - return VfReturnType.VirtualSkip; - } - - //If mfa is enabled, allow processing via mfa - if (MultiFactor != null) - { - if (entity.QueryArgs.ContainsKey("mfa")) - { - return await ProcessMfaAsync(entity); - } - } - return await ProccesLoginAsync(entity); - } - - - private async ValueTask<VfReturnType> ProccesLoginAsync(HttpEntity entity) - { - MfaUpgradeWebm webm = new(); - try - { - //Make sure the id is regenerated (or upgraded if successful login) - entity.Session.RegenID(); - - using LoginMessage? loginMessage = await entity.GetJsonFromFileAsync<LoginMessage>(SR_OPTIONS); - - if (webm.Assert(loginMessage != null, "Invalid request data")) - { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; - } - - //validate the message - if (!LmValidator.Validate(loginMessage, webm)) - { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; - } - - //Time to get the user - using IUser? user = await Users.GetUserAndPassFromEmailAsync(loginMessage.UserName); - //Make sure account exists - if (webm.Assert(user != null, INVALID_MESSAGE)) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //Make sure the account has not been locked out - if (webm.Assert(!UserLoginLocked(user), LOCKED_ACCOUNT_MESSAGE)) - { - goto Cleanup; - } - - //Only allow local accounts - if (user.IsLocalAccount() && !PrivateString.IsNullOrEmpty(user.PassHash)) - { - //If login return true, the response has been set and we should return - if (LoginUser(entity, loginMessage, user, webm)) - { - goto Cleanup; - } - } - - //Inc failed login count - user.FailedLoginIncrement(); - webm.Result = INVALID_MESSAGE; - - Cleanup: - await user.ReleaseAsync(); - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - catch (UserUpdateException uue) - { - 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))) - { - return false; - } - //Reset flc for account - user.FailedLoginCount(0); - try - { - switch (user.Status) - { - case 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; - } - //Gen and store the pw secret - byte[] pwSecret = entity.Session.GenPasswordChallenge(new(loginMessage.Password, false)); - //Encrypt and convert to base64 - string clientPwSecret = EncryptSecret(loginMessage.ClientPublicKey, pwSecret); - //get the new upgrade jwt string - Tuple<string,string>? message = user.MFAGetUpgradeIfEnabled(MultiFactor, loginMessage, clientPwSecret); - //if message is null, mfa was not enabled or could not be prepared - if (message != null) - { - //Store the base64 signature - entity.Session.MfaUpgradeSignature(message.Item2); - //send challenge message to client - webm.Result = message.Item1; - webm.Success = true; - webm.MultiFactorUpgrade = true; - break; - } - //Set password token - webm.PasswordToken = clientPwSecret; - //Elevate the login status of the session to reflect the user's status - webm.Token = entity.GenerateAuthorization(loginMessage, user); - //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]); - } - break; - default: - //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; - } - } - /* - * Account auhorization may throw excetpions if the configuration does not - * match the client, or the client sent invalid or malicous data and - * it could not grant authorization - */ - catch (OutOfMemoryException) - { - webm.Result = "Your browser sent malformatted security information"; - } - catch (CryptographicException ce) - { - webm.Result = "Your browser sent malformatted security information"; - Log.Debug(ce); - } - return true; - } - - - private async ValueTask<VfReturnType> ProcessMfaAsync(HttpEntity entity) - { - MfaUpgradeWebm webm = new(); - //Recover request message - using JsonDocument? request = await entity.GetJsonFromFileAsync(); - if (webm.Assert(request != null, "Invalid request data")) - { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; - } - //Recover upgrade jwt - string? upgradeJwt = request.RootElement.GetPropString("upgrade"); - if (webm.Assert(upgradeJwt != null, "Missing required upgrade data")) - { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; - } - - //Recover stored signature - string? storedSig = entity.Session.MfaUpgradeSignature(); - if(webm.Assert(!string.IsNullOrWhiteSpace(storedSig), MFA_ERROR_MESSAGE)) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //Recover upgrade data from upgrade message - if (!MultiFactor!.RecoverUpgrade(upgradeJwt, storedSig, out MFAUpgrade? upgrade)) - { - webm.Result = MFA_ERROR_MESSAGE; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //recover user account - using IUser? user = await Users.GetUserFromEmailAsync(upgrade.UserName!); - - if (webm.Assert(user != null, MFA_ERROR_MESSAGE)) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - bool locked = UserLoginLocked(user); - - //Make sure the account has not been locked out - if (!webm.Assert(locked == false, LOCKED_ACCOUNT_MESSAGE)) - { - //process mfa login - LoginMfa(entity, user, request, upgrade, webm); - } - else - { - //Locked, so clear stored signature - entity.Session.MfaUpgradeSignature(null); - } - - //Update user on clean process - await user.ReleaseAsync(); - //Close rseponse - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - private void LoginMfa(HttpEntity entity, IUser user, JsonDocument request, MFAUpgrade upgrade, MfaUpgradeWebm webm) - { - //Recover the user's local time - DateTimeOffset localTime = request.RootElement.GetProperty("localtime").GetDateTimeOffset(); - - //Check mode - switch (upgrade.Type) - { - case MFAType.TOTP: - { - //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(); - return; - } - //Valid, complete - } - break; - case MFAType.PGP: - { } - break; - default: - { - webm.Result = MFA_ERROR_MESSAGE; - } - return; - } - - //Wipe session signature - entity.Session.MfaUpgradeSignature(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 - webm.Token = entity.GenerateAuthorization(loginMessage, user); - //Set the password token as the password field of the login message - webm.PasswordToken = upgrade.PwClientData; - //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]); - } - - private static string EncryptSecret(string pubKey, byte[] secret) - { - //Alloc buffer for secret - using IMemoryHandle<byte> buffer = Memory.SafeAlloc<byte>(4096); - //Try to encrypt the data - ERRNO count = TryEncryptClientData(pubKey, secret, buffer.Span); - //Clear secret - RandomHash.GetRandomBytes(secret); - //Convert to base64 string - return Convert.ToBase64String(buffer.Span[..(int)count]); - } - - public bool UserLoginLocked(IUser user) - { - //Recover last counter value - TimestampedCounter flc = user.FailedLoginCount(); - if(flc.Count < MaxFailedLogins) - { - //Period exceeded - return false; - } - //See if the flc timeout period has expired - if (flc.LastModified.Add(FailedCountTimeout) < DateTimeOffset.UtcNow) - { - //clear flc flag - user.FailedLoginCount(0); - return false; - } - //Count has been exceeded, and has not timed out yet - return true; - } - } -}
\ No newline at end of file diff --git a/VNLib.Plugins.Essentials.Accounts/Endpoints/LogoutEndpoint.cs b/VNLib.Plugins.Essentials.Accounts/Endpoints/LogoutEndpoint.cs deleted file mode 100644 index cc36609..0000000 --- a/VNLib.Plugins.Essentials.Accounts/Endpoints/LogoutEndpoint.cs +++ /dev/null @@ -1,53 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Accounts -* File: LogoutEndpoint.cs -* -* LogoutEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Text.Json; -using System.Collections.Generic; - -using VNLib.Plugins.Extensions.Loading; -using VNLib.Plugins.Essentials.Endpoints; - -namespace VNLib.Plugins.Essentials.Accounts.Endpoints -{ - [ConfigurationName("logout_endpoint")] - internal class LogoutEndpoint : ProtectedWebEndpoint - { - - public LogoutEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) - { - string? path = config["path"].GetString(); - InitPathAndLog(path, pbase.Log); - } - - - protected override VfReturnType Post(HttpEntity entity) - { - entity.InvalidateLogin(); - entity.CloseResponse(HttpStatusCode.OK); - return VfReturnType.VirtualSkip; - } - } -} diff --git a/VNLib.Plugins.Essentials.Accounts/Endpoints/MFAEndpoint.cs b/VNLib.Plugins.Essentials.Accounts/Endpoints/MFAEndpoint.cs deleted file mode 100644 index 6ebb024..0000000 --- a/VNLib.Plugins.Essentials.Accounts/Endpoints/MFAEndpoint.cs +++ /dev/null @@ -1,282 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Accounts -* File: MFAEndpoint.cs -* -* MFAEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Text.Json; -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; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Users; -using VNLib.Plugins.Essentials.Extensions; -using VNLib.Plugins.Essentials.Accounts.MFA; -using VNLib.Plugins.Extensions.Validation; -using VNLib.Plugins.Essentials.Endpoints; -using VNLib.Plugins.Extensions.Loading; -using VNLib.Plugins.Extensions.Loading.Users; - -namespace VNLib.Plugins.Essentials.Accounts.Endpoints -{ - [ConfigurationName("mfa_endpoint")] - internal sealed class MFAEndpoint : ProtectedWebEndpoint - { - public const int TOTP_URL_MAX_CHARS = 1024; - - private readonly IUserManager Users; - private readonly MFAConfig? MultiFactor; - - public MFAEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) - { - string? path = config["path"].GetString(); - InitPathAndLog(path, pbase.Log); - - Users = pbase.GetUserManager(); - MultiFactor = pbase.GetMfaConfig(); - } - - private class TOTPUpdateMessage - { - [JsonPropertyName("issuer")] - public string? Issuer { get; set; } - [JsonPropertyName("digits")] - public int Digits { get; set; } - [JsonPropertyName("period")] - public int Period { get; set; } - [JsonPropertyName("secret")] - public string? Base64EncSecret { get; set; } - [JsonPropertyName("algorithm")] - public string? Algorithm { get; set; } - } - - 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; - } - - protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity) - { - WebMessage webm = new(); - - //Get the request message - using JsonDocument? mfaRequest = await entity.GetJsonFromFileAsync(); - if (webm.Assert(mfaRequest != null, "Invalid request")) - { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; - } - - //Get the type argument - string? mfaType = mfaRequest.RootElement.GetPropString("type"); - if (string.IsNullOrWhiteSpace(mfaType)) - { - webm.Result = "MFA type was not specified"; - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; - } - - //Make sure the user's account origin is a local account - if (webm.Assert(entity.Session.HasLocalAccount(), "Your account uses external authentication and MFA cannot be enabled")) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //Make sure mfa is loaded - if (webm.Assert(MultiFactor != null, "MFA is not enabled on this server")) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - //get the user's password challenge - using (PrivateString? password = (PrivateString?)mfaRequest.RootElement.GetPropString("challenge")) - { - if (PrivateString.IsNullOrEmpty(password)) - { - webm.Result = "Please check your password"; - entity.CloseResponseJson(HttpStatusCode.Unauthorized, webm); - return VfReturnType.VirtualSkip; - } - //Verify challenge - if (!entity.Session.VerifyChallenge(password)) - { - 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": - { - //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 - UnsafeMemoryHandle<byte> outputBuffer = Memory.UnsafeAlloc<byte>(4096, true); - try - { - //Encrypt the secret for the client - ERRNO count = entity.Session.TryEncryptClientData(secretBuffer, outputBuffer.Span); - if (!count) - { - webm.Result = "There was an error updating your credentials"; - //If this code is running, the client should have a valid public key stored, but log it anyway - Log.Warn("TOTP secret encryption failed, for requested user {uid}", entity.Session.UserID); - break; - } - webm.Result = new TOTPUpdateMessage() - { - Issuer = MultiFactor.IssuerName, - Digits = MultiFactor.TOTPDigits, - Period = (int)MultiFactor.TOTPPeriod.TotalSeconds, - Algorithm = MultiFactor.TOTPAlg.ToString(), - //Convert the secret to base64 string to send to client - Base64EncSecret = Convert.ToBase64String(outputBuffer.Span[..(int)count]) - }; - //set success flag - webm.Success = true; - } - finally - { - //dispose the output buffer - outputBuffer.Dispose(); - RandomHash.GetRandomBytes(secretBuffer); - } - //Only write changes to the db of operation was successful - await user.ReleaseAsync(); - } - break; - default: - webm.Result = "The server does not support the specified MFA type"; - break; - } - //Close response - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - - protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) - { - WebMessage webm = new(); - try - { - //Check account type - if (!entity.Session.HasLocalAccount()) - { - webm.Result = "You are using external authentication. Operation failed."; - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; - } - - //get the request - using JsonDocument? request = await entity.GetJsonFromFileAsync(); - if (webm.Assert(request != null, "Invalid request.")) - { - 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; - } - //Check for totp disable - if ("totp".Equals(mfaType, StringComparison.OrdinalIgnoreCase)) - { - //Clear the TOTP secret - user.MFASetTOTPSecret(null); - //write changes - await user.ReleaseAsync(); - webm.Result = "Successfully disabled your TOTP authentication"; - webm.Success = true; - } - else if ("fido".Equals(mfaType, StringComparison.OrdinalIgnoreCase)) - { - //Clear webauthn changes - - //write changes - await user.ReleaseAsync(); - webm.Result = "Successfully disabled your FIDO authentication"; - webm.Success = true; - } - else - { - webm.Result = "Invalid MFA type"; - } - //Must write response while password is in scope - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - catch (KeyNotFoundException) - { - webm.Result = "The request was is missing required fields"; - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.BadRequest; - } - } - } -}
\ No newline at end of file diff --git a/VNLib.Plugins.Essentials.Accounts/Endpoints/PasswordResetEndpoint.cs b/VNLib.Plugins.Essentials.Accounts/Endpoints/PasswordResetEndpoint.cs deleted file mode 100644 index 0a51eb5..0000000 --- a/VNLib.Plugins.Essentials.Accounts/Endpoints/PasswordResetEndpoint.cs +++ /dev/null @@ -1,140 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Accounts -* File: PasswordResetEndpoint.cs -* -* PasswordResetEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; - -using FluentValidation; - -using VNLib.Utils.Memory; -using VNLib.Utils.Extensions; -using VNLib.Plugins.Essentials.Users; -using VNLib.Plugins.Essentials.Extensions; -using VNLib.Plugins.Extensions.Validation; -using VNLib.Plugins.Extensions.Loading; -using VNLib.Plugins.Extensions.Loading.Users; -using VNLib.Plugins.Essentials.Endpoints; - -namespace VNLib.Plugins.Essentials.Accounts.Endpoints -{ - - /// <summary> - /// Password reset for user's that are logged in and know - /// their passwords to reset their MFA methods - /// </summary> - [ConfigurationName("password_endpoint")] - internal sealed class PasswordChangeEndpoint : ProtectedWebEndpoint - { - private readonly IUserManager Users; - private readonly PasswordHashing Passwords; - - public PasswordChangeEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) - { - string? path = config["path"].GetString(); - InitPathAndLog(path, pbase.Log); - - Users = pbase.GetUserManager(); - Passwords = pbase.GetPasswords(); - } - - protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) - { - ValErrWebMessage webm = new(); - //get the request body - using JsonDocument? request = await entity.GetJsonFromFileAsync(); - if (request == null) - { - webm.Result = "No request specified"; - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; - } - //get the user's old password - using PrivateString? currentPass = (PrivateString?)request.RootElement.GetPropString("current"); - //Get password as a private string - using PrivateString? newPass = (PrivateString?)request.RootElement.GetPropString("new_password"); - if (PrivateString.IsNullOrEmpty(currentPass)) - { - webm.Result = "You must specifiy your current password."; - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; - } - if (PrivateString.IsNullOrEmpty(newPass)) - { - webm.Result = "You must specifiy a new password."; - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; - } - //Test the password against minimum - if (!AccountValidations.PasswordValidator.Validate((string)newPass, webm)) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - if (webm.Assert(!currentPass.Equals(newPass), "Passwords cannot be the same.")) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - //get the user's entry in the table - using IUser? user = await Users.GetUserAndPassFromIDAsync(entity.Session.UserID); - if(webm.Assert(user != null, "An error has occured, please log-out and try again")) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - //Make sure the account's origin is a local profile - if (webm.Assert(user.IsLocalAccount(), "External accounts cannot be modified")) - { - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - //Verify the user's old password - if (!Passwords.Verify(user.PassHash, currentPass)) - { - webm.Result = "Please check your current password"; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - //Hash the user's new password - using PrivateString newPassHash = Passwords.Hash(newPass); - //Update the user's password - if (!await Users.UpdatePassAsync(user, newPassHash)) - { - //error - webm.Result = "Your password could not be updated"; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - await user.ReleaseAsync(); - //delete the user's MFA entry so they can re-enable it - webm.Result = "Your password has been updated"; - webm.Success = true; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - } -} diff --git a/VNLib.Plugins.Essentials.Accounts/Endpoints/ProfileEndpoint.cs b/VNLib.Plugins.Essentials.Accounts/Endpoints/ProfileEndpoint.cs deleted file mode 100644 index 45908e7..0000000 --- a/VNLib.Plugins.Essentials.Accounts/Endpoints/ProfileEndpoint.cs +++ /dev/null @@ -1,132 +0,0 @@ -/* -* Copyright (c) 2022 Vaughn Nugent -* -* Library: VNLib -* Package: VNLib.Plugins.Essentials.Accounts -* File: ProfileEndpoint.cs -* -* ProfileEndpoint.cs is part of VNLib.Plugins.Essentials.Accounts which is part of the larger -* VNLib collection of libraries and utilities. -* -* VNLib.Plugins.Essentials.Accounts is free software: you can redistribute it and/or modify -* it under the terms of the GNU Affero General Public License as -* published by the Free Software Foundation, either version 3 of the -* License, or (at your option) any later version. -* -* VNLib.Plugins.Essentials.Accounts is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Affero General Public License for more details. -* -* You should have received a copy of the GNU Affero General Public License -* along with this program. If not, see https://www.gnu.org/licenses/. -*/ - -using System; -using System.Net; -using System.Text.Json; -using System.Threading.Tasks; -using System.Collections.Generic; - -using VNLib.Utils.Logging; -using VNLib.Plugins.Essentials.Users; -using VNLib.Plugins.Essentials.Endpoints; -using VNLib.Plugins.Essentials.Extensions; -using VNLib.Plugins.Extensions.Validation; -using VNLib.Plugins.Extensions.Loading; -using VNLib.Plugins.Extensions.Loading.Users; -using static VNLib.Plugins.Essentials.Statics; - - -namespace VNLib.Plugins.Essentials.Accounts.Endpoints -{ - /// <summary> - /// Provides an http endpoint for user account profile access - /// </summary> - [ConfigurationName("profile_endpoint")] - internal sealed class ProfileEndpoint : ProtectedWebEndpoint - { - private readonly IUserManager Users; - - public ProfileEndpoint(PluginBase pbase, IReadOnlyDictionary<string, JsonElement> config) - { - string? path = config["path"].GetString(); - - InitPathAndLog(path, pbase.Log); - //Store user system - Users = pbase.GetUserManager(); - } - - protected override async ValueTask<VfReturnType> GetAsync(HttpEntity entity) - { - //get user data from database - using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); - //Make sure the account exists - if (user == null || user.Status != UserStatus.Active) - { - //Account was not found - entity.CloseResponse(HttpStatusCode.NotFound); - return VfReturnType.VirtualSkip; - } - //Get the stored profile - AccountData? profile = user.GetProfile(); - //No profile found, so return an empty "profile" - profile ??= new() - { - //set email address - EmailAddress = user.EmailAddress, - //created time in rfc1123 gmt time - Created = user.Created.ToString("R") - }; - //Serialize the profile and return to user - entity.CloseResponseJson(HttpStatusCode.OK, profile); - return VfReturnType.VirtualSkip; - } - protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity) - { - ValErrWebMessage webm = new(); - try - { - //Recover the update message form the client - AccountData? updateMessage = await entity.GetJsonFromFileAsync<AccountData>(SR_OPTIONS); - if (webm.Assert(updateMessage != null, "Malformatted payload")) - { - entity.CloseResponseJson(HttpStatusCode.BadRequest, webm); - return VfReturnType.VirtualSkip; - } - //Validate the new account data - if (!AccountValidations.AccountDataValidator.Validate(updateMessage, webm)) - { - entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm); - return VfReturnType.VirtualSkip; - } - //Get the user from database - using IUser? user = await Users.GetUserFromIDAsync(entity.Session.UserID); - //Make sure the user exists - if (webm.Assert(user != null, "Account does not exist")) - { - //Should probably log the user out here - entity.CloseResponseJson(HttpStatusCode.NotFound, webm); - return VfReturnType.VirtualSkip; - } - //Overwite the current profile data (will also sanitize inputs) - user.SetProfile(updateMessage); - //Update the user only if successful - await user.ReleaseAsync(); - webm.Result = "Successfully updated account"; - webm.Success = true; - entity.CloseResponse(webm); - return VfReturnType.VirtualSkip; - } - //Catch an account update exception - catch (UserUpdateException uue) - { - Log.Error(uue, "An error occured while the user account is being updated"); - //Return message to client - webm.Result = "An error occured while updating your account, try again later"; - entity.CloseResponseJson(HttpStatusCode.InternalServerError, webm); - return VfReturnType.VirtualSkip; - } - } - } -}
\ No newline at end of file |