aboutsummaryrefslogtreecommitdiff
path: root/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts.Registration/src')
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/AccountValidations.cs132
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs40
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs368
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs39
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationEntryPoint.cs66
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs43
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs101
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj56
8 files changed, 845 insertions, 0 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/AccountValidations.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/AccountValidations.cs
new file mode 100644
index 0000000..b84728b
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/AccountValidations.cs
@@ -0,0 +1,132 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts.Registration
+* File: AccountValidations.cs
+*
+* AccountValidations.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 FluentValidation;
+
+using VNLib.Plugins.Essentials.Accounts.Registration.Endpoints;
+using VNLib.Plugins.Extensions.Validation;
+
+namespace VNLib.Plugins.Essentials.Accounts.Registration
+{
+ internal static class AccountValidations
+ {
+ /// <summary>
+ /// Central password requirement validator
+ /// </summary>
+ public static IValidator<string> PasswordValidator { get; } = GetPassVal();
+
+ public static IValidator<AccountData> AccountDataValidator { get; } = GetAcVal();
+
+ /// <summary>
+ /// A validator used to validate new registration request messages
+ /// </summary>
+ public static IValidator<RegRequestMessage> RegRequestValidator { get; } = GetRequestValidator();
+
+ static IValidator<string> GetPassVal()
+ {
+ InlineValidator<string> passVal = new();
+
+ passVal.RuleFor(static password => password)
+ .NotEmpty()
+ .Length(min: 8, max: 100)
+ .Password()
+ .WithMessage(errorMessage: "Password does not meet minium requirements");
+
+ return passVal;
+ }
+
+ static IValidator<AccountData> GetAcVal()
+ {
+ InlineValidator<AccountData> adv = new ();
+
+ //Validate city
+
+ adv.RuleFor(t => t.City)
+ .MaximumLength(35)
+ .AlphaOnly()
+ .When(t => t.City?.Length > 0);
+
+ adv.RuleFor(t => t.Company)
+ .MaximumLength(50)
+ .SpecialCharacters()
+ .When(t => t.Company?.Length > 0);
+
+ //Require a first and last names to be set together
+ adv.When(t => t.First?.Length > 0 || t.Last?.Length > 0, () =>
+ {
+ adv.RuleFor(t => t.First)
+ .Length(1, 35)
+ .AlphaOnly();
+ adv.RuleFor(t => t.Last)
+ .Length(1, 35)
+ .AlphaOnly();
+ });
+
+ adv.RuleFor(t => t.PhoneNumber)
+ .PhoneNumber()
+ .When(t => t.PhoneNumber?.Length > 0)
+ .OverridePropertyName("Phone");
+
+ //State must be 2 characters for us states if set
+ adv.RuleFor(t => t.State)
+ .Length(2)
+ .When(t => t.State?.Length > 0);
+
+ adv.RuleFor(t => t.Street)
+ .AlphaNumericOnly()
+ .MaximumLength(50)
+ .When(t => t.Street?.Length > 0);
+
+ //Allow empty zip codes, but if one is defined, is must be less than 7 characters
+ adv.RuleFor(t => t.Zip)
+ .NumericOnly()
+ .MaximumLength(7)
+ .When(t => t.Zip?.Length > 0);
+
+ return adv;
+ }
+
+ static IValidator<RegRequestMessage> GetRequestValidator()
+ {
+ InlineValidator<RegRequestMessage> reqVal = new();
+
+ reqVal.RuleFor(static s => s.ClientId)
+ .NotEmpty()
+ .AlphaNumericOnly()
+ .Length(1, 100);
+
+ //Convert to universal time before validating
+ reqVal.RuleFor(static s => s.Timestamp.ToUniversalTime())
+ .Must(t => t > DateTimeOffset.UtcNow.AddSeconds(-60) && t < DateTimeOffset.UtcNow.AddSeconds(60));
+
+ reqVal.RuleFor(static s => s.UserName)
+ .NotEmpty()
+ .EmailAddress()
+ .IllegalCharacters()
+ .Length(5, 50);
+
+ return reqVal;
+ }
+ }
+}
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs
new file mode 100644
index 0000000..0683067
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegRequestMessage.cs
@@ -0,0 +1,40 @@
+/*
+* 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/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
new file mode 100644
index 0000000..5c22344
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/Endpoints/RegistrationEntpoint.cs
@@ -0,0 +1,368 @@
+/*
+* 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 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.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.Extentions.TransactionalEmail;
+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 readonly IUserManager Users;
+ private readonly IValidator<string> RegJwtValdidator;
+ private readonly PasswordHashing Passwords;
+ private readonly RevokedTokenStore RevokedTokens;
+ private readonly TransactionalEmailConfig Emails;
+ private readonly Task<ReadOnlyJsonWebKey> 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 = plugin.GetEmailConfig();
+
+ //Begin the async op to get the signature key from the vault
+ RegSignatureKey = plugin.TryGetSecretAsync("reg_sig_key").ToJsonWebKey(true);
+
+ //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
+ bool verified = jwt.VerifyFromJwk(RegSignatureKey.Result);
+
+ 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;
+ }
+
+ 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(RegSignatureKey.Result.JwtHeader);
+
+ //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
+ emailJwt.SignFromJwk(RegSignatureKey.Result);
+ //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.GetTemplateRequest("Registration");
+ //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"));
+
+ //Send the email
+ TransactionResult result = await Emails.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
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs
new file mode 100644
index 0000000..611e30e
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationContext.cs
@@ -0,0 +1,39 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts.Registration
+* File: RevocationContext.cs
+*
+* RevocationContext.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 Microsoft.EntityFrameworkCore;
+
+using VNLib.Plugins.Extensions.Data;
+using VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation;
+
+namespace VNLib.Plugins.Essentials.Accounts.Registration
+{
+ internal class RegistrationContext : TransactionalDbContext
+ {
+ public DbSet<RevokedToken> RevokedRegistrationTokens { get; set; }
+
+ public RegistrationContext(DbContextOptions options) : base(options)
+ {}
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationEntryPoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationEntryPoint.cs
new file mode 100644
index 0000000..c24e7e0
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/RegistrationEntryPoint.cs
@@ -0,0 +1,66 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts.Registration
+* File: RegistrationEntryPoint.cs
+*
+* RegistrationEntryPoint.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 VNLib.Utils.Logging;
+
+using VNLib.Plugins.Extensions.Loading;
+using VNLib.Plugins.Extensions.Loading.Sql;
+using VNLib.Plugins.Extensions.Loading.Routing;
+using VNLib.Plugins.Essentials.Accounts.Registration.Endpoints;
+
+namespace VNLib.Plugins.Essentials.Accounts.Registration
+{
+ public sealed class RegistrationEntryPoint : PluginBase
+ {
+ public override string PluginName => "Essentials.EmailRegistration";
+
+ protected override void OnLoad()
+ {
+ try
+ {
+ //Route reg endpoint
+ this.Route<RegistrationEntpoint>();
+
+ Log.Information("Plugin loaded");
+ }
+ catch(KeyNotFoundException kne)
+ {
+ Log.Error("Missing required configuration variables: {ex}", kne.Message);
+ }
+ }
+
+ protected override void OnUnLoad()
+ {
+ Log.Information("Plugin unloaded");
+ }
+
+ protected override void ProcessHostCommand(string cmd)
+ {
+ if (!this.IsDebug())
+ {
+ return;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs
new file mode 100644
index 0000000..c2b7715
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedToken.cs
@@ -0,0 +1,43 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts.Registration
+* File: RevokedToken.cs
+*
+* RevokedToken.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.ComponentModel.DataAnnotations;
+
+namespace VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation
+{
+
+ internal class RevokedToken
+ {
+ /// <summary>
+ /// The time the token was revoked.
+ /// </summary>
+ public DateTime Created { get; set; }
+ /// <summary>
+ /// The token that was revoked.
+ /// </summary>
+ [Key]
+ public string? Token { get; set; }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs
new file mode 100644
index 0000000..89f4bd6
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/TokenRevocation/RevokedTokenStore.cs
@@ -0,0 +1,101 @@
+/*
+* Copyright (c) 2022 Vaughn Nugent
+*
+* Library: VNLib
+* Package: VNLib.Plugins.Essentials.Accounts.Registration
+* File: RevokedTokenStore.cs
+*
+* RevokedTokenStore.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.Collections;
+
+using Microsoft.EntityFrameworkCore;
+
+using VNLib.Utils;
+
+namespace VNLib.Plugins.Essentials.Accounts.Registration.TokenRevocation
+{
+ internal class RevokedTokenStore
+ {
+ private readonly DbContextOptions Options;
+
+ public RevokedTokenStore(DbContextOptions options)
+ {
+ Options = options;
+ }
+
+ public async Task<bool> IsRevokedAsync(string token, CancellationToken cancellation)
+ {
+ await using RegistrationContext context = new (Options);
+ await context.OpenTransactionAsync(cancellation);
+
+ //Select any that match tokens
+ bool any = await (from t in context.RevokedRegistrationTokens
+ where t.Token == token
+ select t).AnyAsync(cancellation);
+
+ await context.CommitTransactionAsync(cancellation);
+ return any;
+ }
+
+ public async Task RevokeAsync(string token, CancellationToken cancellation)
+ {
+ await using RegistrationContext context = new (Options);
+ await context.OpenTransactionAsync(cancellation);
+
+ //Add to table
+ context.RevokedRegistrationTokens.Add(new RevokedToken()
+ {
+ Created = DateTime.UtcNow,
+ Token = token
+ });
+
+ //Save changes and commit transaction
+ await context.SaveChangesAsync(cancellation);
+ await context.CommitTransactionAsync(cancellation);
+ }
+
+ /// <summary>
+ /// Removes expired records from the store
+ /// </summary>
+ /// <param name="validFor">The time a token is valid for</param>
+ /// <param name="cancellation">A token that cancels the async operation</param>
+ /// <returns>The number of records evicted from the store</returns>
+ public async Task<ERRNO> CleanTableAsync(TimeSpan validFor, CancellationToken cancellation)
+ {
+ DateTime expiredBefore = DateTime.UtcNow.Subtract(validFor);
+
+ await using RegistrationContext context = new (Options);
+ await context.OpenTransactionAsync(cancellation);
+
+ //Select any that match tokens
+ RevokedToken[] expired = await context.RevokedRegistrationTokens.Where(t => t.Created < expiredBefore)
+ .Select(static t => t)
+ .ToArrayAsync(cancellation);
+
+
+ context.RevokedRegistrationTokens.RemoveRange(expired);
+
+ ERRNO count =await context.SaveChangesAsync(cancellation);
+
+ await context.CommitTransactionAsync(cancellation);
+
+ return count;
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj
new file mode 100644
index 0000000..981d252
--- /dev/null
+++ b/plugins/VNLib.Plugins.Essentials.Accounts.Registration/src/VNLib.Plugins.Essentials.Accounts.Registration.csproj
@@ -0,0 +1,56 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <TargetFramework>net6.0</TargetFramework>
+ <ImplicitUsings>enable</ImplicitUsings>
+ <Nullable>enable</Nullable>
+ <GenerateDocumentationFile>False</GenerateDocumentationFile>
+ <Title>VNLib.Plugins.Essentials.Accounts.Registration</Title>
+ <Authors>Vaughn Nugent</Authors>
+ <Copyright>Copyright © 2022 Vaughn Nugent</Copyright>
+ <PackageProjectUrl>https://www.vaughnnugent.com/resources</PackageProjectUrl>
+ <ProduceReferenceAssembly>False</ProduceReferenceAssembly>
+ <SignAssembly>False</SignAssembly>
+ <AssemblyVersion>1.0.0.1</AssemblyVersion>
+ <AssemblyName>Essentials.EmailRegistration</AssemblyName>
+ <CheckForOverflowUnderflow>False</CheckForOverflowUnderflow>
+ <SignAssembly>True</SignAssembly>
+ <AssemblyOriginatorKeyFile>\\vaughnnugent.com\Internal\Folder Redirection\vman\Documents\Programming\Software\StrongNameingKey.snk</AssemblyOriginatorKeyFile>
+ </PropertyGroup>
+
+ <!-- Resolve nuget dll files and store them in the output dir -->
+ <PropertyGroup>
+ <!--Enable dynamic loading-->
+ <EnableDynamicLoading>true</EnableDynamicLoading>
+ <AnalysisLevel>latest-all</AnalysisLevel>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+ <Deterministic>False</Deterministic>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="FluentValidation" Version="11.4.0" />
+ </ItemGroup>
+ <ItemGroup>
+ <ProjectReference Include="..\..\..\..\..\core\lib\Plugins.Essentials\src\VNLib.Plugins.Essentials.csproj" />
+ <ProjectReference Include="..\..\..\..\Emails.Transactional\lib\Emails.Transactional.Client\src\Emails.Transactional.Client.csproj" />
+ <ProjectReference Include="..\..\..\..\Emails.Transactional\lib\Emails.Transactional.Extensions\src\VNLib.Plugins.Extentions.TransactionalEmail.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Data\src\VNLib.Plugins.Extensions.Data.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading.Sql\src\VNLib.Plugins.Extensions.Loading.Sql.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Loading\src\VNLib.Plugins.Extensions.Loading.csproj" />
+ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj" />
+ </ItemGroup>
+
+ <ItemGroup>
+ <None Update="Essentials.EmailRegistration.json">
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+ </None>
+ </ItemGroup>
+
+ <Target Name="PostBuild" AfterTargets="PostBuildEvent">
+ <Exec Command="start xcopy &quot;$(TargetDir)&quot; &quot;F:\Programming\vnlib\devplugins\$(TargetName)&quot; /E /Y /R" />
+ </Target>
+
+</Project>