aboutsummaryrefslogtreecommitdiff
path: root/VNLib.Plugins.Essentials.Accounts.Registration/src/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.Registration/src/Endpoints
parentb6481038bc6573af30492e9ce52b36d9f64195f3 (diff)
Large project reorder and consolidation
Diffstat (limited to 'VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints')
-rw-r--r--VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs40
-rw-r--r--VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs392
2 files changed, 0 insertions, 432 deletions
diff --git a/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs b/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs
deleted file mode 100644
index 0683067..0000000
--- a/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts.Registration
-* File: RegRequestMessage.cs
-*
-* RegRequestMessage.cs is part of VNLib.Plugins.Essentials.Accounts.Registration which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts.Registration 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.Registration 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.Text.Json.Serialization;
-
-namespace VNLib.Plugins.Essentials.Accounts.Registration.Endpoints
-{
- internal class RegRequestMessage
- {
- [JsonPropertyName("localtime")]
- public DateTimeOffset Timestamp { get; set; }
-
- [JsonPropertyName("username")]
- public string? UserName { get; set; }
-
- [JsonPropertyName("clientid")]
- public string? ClientId { get; set; }
- }
-} \ No newline at end of file
diff --git a/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs b/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
deleted file mode 100644
index 74f2aa1..0000000
--- a/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
+++ /dev/null
@@ -1,392 +0,0 @@
-/*
-* Copyright (c) 2022 Vaughn Nugent
-*
-* Library: VNLib
-* Package: VNLib.Plugins.Essentials.Accounts.Registration
-* File: RegistrationEntpoint.cs
-*
-* RegistrationEntpoint.cs is part of VNLib.Plugins.Essentials.Accounts.Registration which is part of the larger
-* VNLib collection of libraries and utilities.
-*
-* VNLib.Plugins.Essentials.Accounts.Registration 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.Registration 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.Security.Cryptography;
-
-using FluentValidation;
-
-using Emails.Transactional.Client;
-using Emails.Transactional.Client.Exceptions;
-
-using VNLib.Hashing;
-using VNLib.Utils.Memory;
-using VNLib.Utils.Logging;
-using VNLib.Utils.Extensions;
-using VNLib.Hashing.IdentityUtility;
-using VNLib.Net.Rest.Client;
-using VNLib.Net.Rest.Client.OAuth2;
-using VNLib.Plugins.Essentials.Users;
-using VNLib.Plugins.Essentials.Endpoints;
-using VNLib.Plugins.Essentials.Extensions;
-using VNLib.Plugins.Extensions.Loading;
-using VNLib.Plugins.Extensions.Loading.Sql;
-using VNLib.Plugins.Extensions.Loading.Events;
-using VNLib.Plugins.Extensions.Loading.Users;
-using VNLib.Plugins.Extensions.Validation;
-using VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation;
-using static VNLib.Plugins.Essentials.Accounts.AccountManager;
-
-
-namespace VNLib.Plugins.Essentials.Accounts.Registration.Endpoints
-{
-
- [ConfigurationName("registration")]
- internal sealed class RegistrationEntpoint : UnprotectedWebEndpoint, IIntervalScheduleable
- {
- /// <summary>
- /// Generates a CNG random buffer to use as a nonce
- /// </summary>
- private static string EntropyNonce => RandomHash.GetRandomHex(16);
-
- const string FAILED_AUTH_ERR = "Your registration does not exist, you should try to regisiter again.";
- const string REG_ERR_MESSAGE = "Please check your email inbox.";
-
- private HMAC SigAlg => new HMACSHA256(RegSignatureKey.Result);
-
- private readonly IUserManager Users;
- private readonly IValidator<string> RegJwtValdidator;
- private readonly PasswordHashing Passwords;
- private readonly RevokedTokenStore RevokedTokens;
- private readonly EmailSystemConfig Emails;
- private readonly Task<byte[]> RegSignatureKey;
- private readonly TimeSpan RegExpiresSec;
-
- /// <summary>
- /// Creates back-end functionality for a "registration" or "sign-up" page that integrates with the <see cref="AccountManager"/> plugin
- /// </summary>
- /// <param name="Path">The path identifier</param>
- /// <exception cref="ArgumentException"></exception>
- public RegistrationEntpoint(PluginBase plugin, IReadOnlyDictionary<string, JsonElement> config)
- {
- string? path = config["path"].GetString();
-
- InitPathAndLog(path, plugin.Log);
-
- RegExpiresSec = config["reg_expires_sec"].GetTimeSpan(TimeParseType.Seconds);
-
- //Init reg jwt validator
- RegJwtValdidator = GetJwtValidator();
-
- Passwords = plugin.GetPasswords();
- Users = plugin.GetUserManager();
- RevokedTokens = new(plugin.GetContextOptions());
- Emails = new(plugin);
-
- //Begin the async op to get the signature key from the vault
- RegSignatureKey = plugin.TryGetSecretAsync("reg_sig_key").ContinueWith((ts) => {
-
- using SecretResult? sr = ts.Result ?? throw new KeyNotFoundException("Missing required key 'reg_sig_key' in 'registration' configuration");
- return ts.Result.GetFromBase64();
-
- }, TaskScheduler.Default);
-
- //Register timeout for cleanup
- plugin.ScheduleInterval(this, TimeSpan.FromSeconds(60));
- }
-
- private static IValidator<string> GetJwtValidator()
- {
- InlineValidator<string> val = new();
-
- val.RuleFor(static s => s)
- .NotEmpty()
- //Must contain 2 periods for jwt limitation
- .Must(static s => s.Count(s => s == '.') == 2)
- //Guard length
- .Length(20, 500)
- .IllegalCharacters();
- return val;
- }
-
-
- protected override async ValueTask<VfReturnType> PostAsync(HttpEntity entity)
- {
- ValErrWebMessage webm = new();
- //Get the json request data from client
- using JsonDocument? request = await entity.GetJsonFromFileAsync();
-
- if(webm.Assert(request != null, "No request data present"))
- {
- entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Get the jwt string from client
- string? regJwt = request.RootElement.GetPropString("token");
- using PrivateString? password = (PrivateString?)request.RootElement.GetPropString("password");
-
- //validate inputs
- {
- if (webm.Assert(regJwt != null, FAILED_AUTH_ERR))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- if (webm.Assert(password != null, "You must specify a password."))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- //validate new password
- if(!AccountValidations.PasswordValidator.Validate((string)password, webm))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- //Validate jwt
- if (webm.Assert(RegJwtValdidator.Validate(regJwt).IsValid, FAILED_AUTH_ERR))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- }
-
- //Verify jwt has not been revoked
- if(await RevokedTokens.IsRevokedAsync(regJwt, entity.EventCancellation))
- {
- webm.Result = FAILED_AUTH_ERR;
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
- string emailAddress;
- try
- {
- //get jwt
- using JsonWebToken jwt = JsonWebToken.Parse(regJwt);
- //verify signature
- using (HMAC hmac = SigAlg)
- {
- bool verified = jwt.Verify(hmac);
-
- if (webm.Assert(verified, FAILED_AUTH_ERR))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- }
-
- //recover iat and email address
- using JsonDocument reg = jwt.GetPayload();
- emailAddress = reg.RootElement.GetPropString("email")!;
- DateTimeOffset iat = DateTimeOffset.FromUnixTimeSeconds(reg.RootElement.GetProperty("iat").GetInt64());
-
- //Verify IAT against expiration at second resolution
- if (webm.Assert(iat.Add(RegExpiresSec) > DateTimeOffset.UtcNow, FAILED_AUTH_ERR))
- {
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- }
- catch (FormatException fe)
- {
- Log.Debug(fe);
- webm.Result = FAILED_AUTH_ERR;
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
-
- //Always hash the new password, even if failed
- using PrivateString passHash = Passwords.Hash(password);
-
- try
- {
- //Generate userid from email
- string uid = GetRandomUserId();
-
- //Create the new user
- using IUser user = await Users.CreateUserAsync(uid, emailAddress, MINIMUM_LEVEL, passHash, entity.EventCancellation);
-
- //Set active status
- user.Status = UserStatus.Active;
- //set local account origin
- user.SetAccountOrigin(LOCAL_ACCOUNT_ORIGIN);
-
- //set user verification
- await user.ReleaseAsync();
-
- //Revoke token now complete
- _ = RevokedTokens.RevokeAsync(regJwt, CancellationToken.None).ConfigureAwait(false);
-
- webm.Result = "Successfully created your new account. You may now log in";
- webm.Success = true;
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
- //Capture creation failed, this may be a replay
- catch (UserExistsException)
- {
- }
- catch(UserCreationFailedException)
- {
- }
-
- webm.Result = FAILED_AUTH_ERR;
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
-
- private static readonly IReadOnlyDictionary<string, string> JWT_HEADER = new Dictionary<string, string>()
- {
- { "typ", "JWT" },
- { "alg", "HS256" }
- };
-
- protected override async ValueTask<VfReturnType> PutAsync(HttpEntity entity)
- {
- ValErrWebMessage webm = new();
-
- //Get the request
- RegRequestMessage? request = await entity.GetJsonFromFileAsync<RegRequestMessage>();
- if (webm.Assert(request != null, "Request is invalid"))
- {
- entity.CloseResponseJson(HttpStatusCode.BadRequest, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Validate the request
- if (!AccountValidations.RegRequestValidator.Validate(request, webm))
- {
- entity.CloseResponseJson(HttpStatusCode.UnprocessableEntity, webm);
- return VfReturnType.VirtualSkip;
- }
-
- //Create psudo contant time delay
- Task delay = Task.Delay(200);
-
- //See if a user account already exists
- using (IUser? user = await Users.GetUserFromEmailAsync(request.UserName!, entity.EventCancellation))
- {
- if (user != null)
- {
- goto Exit;
- }
- }
-
- //Get exact timestamp
- DateTimeOffset timeStamp = DateTimeOffset.UtcNow;
-
- //generate random nonce for entropy
- string entropy = EntropyNonce;
-
- //Init client jwt
- string jwtData;
- using (JsonWebToken emailJwt = new())
- {
-
- emailJwt.WriteHeader(JWT_HEADER);
-
- //Init new claim stack, include the same iat time, nonce for entropy, and descriptor storage id
- emailJwt.InitPayloadClaim(3)
- .AddClaim("iat", timeStamp.ToUnixTimeSeconds())
- .AddClaim("n", entropy)
- .AddClaim("email", request.UserName)
- .CommitClaims();
-
- //sign the jwt
- using (HMAC hmac = SigAlg)
- {
- emailJwt.Sign(hmac);
- }
- //Compile to encoded string
- jwtData = emailJwt.Compile();
- }
-
- string regUrl = $"https://{entity.Server.RequestUri.Authority}{Path}?t={jwtData}";
-
- //Send email to user in background task and do not await it
- _ = SendRegEmailAsync(request.UserName!, regUrl).ConfigureAwait(false);
-
- Exit:
- //await sort of constant time delay
- await delay;
-
- //Notify user
- webm.Result = REG_ERR_MESSAGE;
- webm.Success = true;
-
- entity.CloseResponse(webm);
- return VfReturnType.VirtualSkip;
- }
-
-
- private async Task SendRegEmailAsync(string emailAddress, string url)
- {
- try
- {
- //Get a new registration template
- EmailTransactionRequest emailTemplate = Emails.GetRegistrationMessage();
- //Add the user's to address
- emailTemplate.AddToAddress(emailAddress);
- emailTemplate.AddVariable("username", emailAddress);
- //Set the security code variable string
- emailTemplate.AddVariable("reg_url", url);
- emailTemplate.AddVariable("date", DateTimeOffset.UtcNow.ToString("f"));
-
- //Get a new client contract
- using ClientContract client = Emails.RestClientPool.Lease();
- //Send the email
- TransactionResult result = await client.Resource.SendEmailAsync(emailTemplate);
- if (!result.Success)
- {
- Log.Debug("Registration email failed to send, SMTP status code: {smtp}", result.SmtpStatus);
- }
- else
- {
- Log.Verbose("Registration email sent to user. Status {smtp}", result.SmtpStatus);
- }
- }
- catch (ValidationFailedException vf)
- {
- //This should only occur if there is a bug in our reigration code that allowed an invalid value pass
- Log.Debug(vf, "Registration email failed to send to user because data validation failed");
- }
- catch (InvalidAuthorizationException iae)
- {
- Log.Warn(iae, "Registration email failed to send due to an authentication error");
- }
- catch (OAuth2AuthenticationException o2e)
- {
- Log.Warn(o2e, "Registration email failed to send due to an authentication error");
- }
- catch (Exception ex)
- {
- Log.Error(ex);
- }
- }
-
- async Task IIntervalScheduleable.OnIntervalAsync(ILogProvider log, CancellationToken cancellationToken)
- {
- //Cleanup tokens
- await RevokedTokens.CleanTableAsync(RegExpiresSec, cancellationToken);
- }
- }
-} \ No newline at end of file