diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts.Registration/src')
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 "$(TargetDir)" "F:\Programming\vnlib\devplugins\$(TargetName)" /E /Y /R" /> + </Target> + +</Project> |