aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Essentials.Accounts/Endpoints
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:40 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2023-01-12 17:47:40 -0500
commit551066ed9a255bd47c1c5789ec1998fda64bd5aa (patch)
treed6caceb0e7caa44478c6611903b4b7e120964c89 /VNLib.Plugins.Essentials.Accounts/Endpoints
parentb6481038bc6573af30492e9ce52b36d9f64195f3 (diff)
Large project reorder and consolidation
Diffstat (limited to 'VNLib.Plugins.Essentials.Accounts/Endpoints')
-rw-r--r--VNLib.Plugins.Essentials.Accounts/Endpoints/KeepAliveEndpoint.cs64
-rw-r--r--VNLib.Plugins.Essentials.Accounts/Endpoints/LoginEndpoint.cs410
-rw-r--r--VNLib.Plugins.Essentials.Accounts/Endpoints/LogoutEndpoint.cs53
-rw-r--r--VNLib.Plugins.Essentials.Accounts/Endpoints/MFAEndpoint.cs282
-rw-r--r--VNLib.Plugins.Essentials.Accounts/Endpoints/PasswordResetEndpoint.cs140
-rw-r--r--VNLib.Plugins.Essentials.Accounts/Endpoints/ProfileEndpoint.cs132
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