diff options
Diffstat (limited to 'plugins/VNLib.Plugins.Essentials.Accounts/src')
4 files changed, 177 insertions, 47 deletions
diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs index 33c72a7..60c99e3 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs @@ -1,5 +1,5 @@ /* -* Copyright (c) 2023 Vaughn Nugent +* Copyright (c) 2024 Vaughn Nugent * * Library: VNLib * Package: VNLib.Plugins.Essentials.Accounts @@ -60,7 +60,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints internal sealed class PasswordChangeEndpoint : ProtectedWebEndpoint { private readonly IUserManager Users; - private readonly MFAConfig? mFAConfig; + private readonly MFAConfig mFAConfig; private readonly IValidator<PasswordResetMesage> ResetMessValidator; public PasswordChangeEndpoint(PluginBase pbase, IConfigScope config) @@ -87,7 +87,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints .NotEmpty() .NotEqual(static pm => pm.Current) .WithMessage("Your new password may not equal your new current password") - .SetValidator(AccountValidations.PasswordValidator!); + .SetValidator(AccountValidations.PasswordValidator); return rules; } @@ -134,29 +134,27 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } //Check if totp is enabled - if (user.MFATotpEnabled()) + if (mFAConfig.TOTPEnabled && user.MFATotpEnabled()) { - if(mFAConfig != null) + //TOTP code is required + if (webm.Assert(pwReset.TotpCode.HasValue, "TOTP is enabled on this user account, you must enter your TOTP code.")) { - //TOTP code is required - if(webm.Assert(pwReset.TotpCode.HasValue, "TOTP is enabled on this user account, you must enter your TOTP code.")) - { - return VirtualOk(entity, webm); - } - - //Veriy totp code - bool verified = mFAConfig.VerifyTOTP(user, pwReset.TotpCode.Value); - - if (webm.Assert(verified, "Please check your TOTP code and try again")) - { - return VirtualOk(entity, webm); - } + return VirtualOk(entity, webm); } + + //Veriy totp code + bool verified = mFAConfig.VerifyTOTP(user, pwReset.TotpCode.Value); + + if (webm.Assert(verified, "Please check your TOTP code and try again")) + { + return VirtualOk(entity, webm); + } + //continue } //Update the user's password - if (!await Users.UpdatePasswordAsync(user, pwReset.NewPassword!, entity.EventCancellation)) + if (await Users.UpdatePasswordAsync(user, pwReset.NewPassword!, entity.EventCancellation) == 1) { //error webm.Result = "Your password could not be updated"; @@ -164,7 +162,7 @@ namespace VNLib.Plugins.Essentials.Accounts.Endpoints } //Publish to user database - await user.ReleaseAsync(); + await user.ReleaseAsync(entity.EventCancellation); //delete the user's MFA entry so they can re-enable it webm.Result = "Your password has been updated"; diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/Essentials.Accounts.json b/plugins/VNLib.Plugins.Essentials.Accounts/src/Essentials.Accounts.json new file mode 100644 index 0000000..73529ee --- /dev/null +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/Essentials.Accounts.json @@ -0,0 +1,94 @@ +{ + "debug": false, + + //endpoints (any or all can be commented out if not needed) + + "login_endpoint": { + "path": "/account/login", + "max_login_attempts": 10, //10 failed attempts in 10 minutes + "failed_attempt_timeout_sec": 600 //10 minutes + }, + + "keepalive_endpoint": { + "path": "/account/keepalive", + //Regen token every 15 mins along with cookies + "token_refresh_sec": 600 //15 minutes + }, + + "profile_endpoint": { + "path": "/account/profile" + }, + + "password_endpoint": { + "path": "/account/reset" + }, + + "mfa_endpoint": { + "path": "/account/mfa" + }, + + "logout_endpoint": { + "path": "/account/logout" + }, + + "pki_auth_endpoint": { + "path": "/account/pki", + "jwt_time_dif_sec": 30, + + "max_login_attempts": 10, + "failed_attempt_timeout_sec": 600, + + //Configures the PATCH and DELETE methods to update the user's stored key when logged in + "enable_key_update": true + }, + + //If mfa is defined, configures mfa enpoints and enables mfa logins + "mfa": { + "upgrade_expires_secs": 180, + "nonce_size": 64, + + //Defines totp specific arguments + "totp": { + "digits": 6, + "issuer": "vaughnnugent.com", + "period_secs": 30, + "algorithm": "sha1", + "secret_size": 32, + "window_size": 2 + }, + + "fido": { + "challenge_size": 64, + "attestation": "none", + "timeout": 60000, + "site_name": "vaughnnugent.com", + + "authenticatorSelection": { + "authenticatorAttachment": "cross-platform", + "requireResidentKey": false, + "userVerification": "required" + } + } + }, + + //Defines the included account provider + "account_security": { + //Time in seconds before a session is considered expired + "session_valid_for_sec": 3600, + + //Path/domain for all security cookies + "cookie_domain": "", + "cookie_path": "/", + + "status_cookie_name": "li", + + "otp_header_name": "X-Web-Token", + "otp_time_diff_sec": 30, + "otp_key_size": 64, + + "pubkey_cookie_name": "client-id", + "pubkey_signing_key_size": 32, + + "strict_origin": false + } +}
\ No newline at end of file diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs b/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs index f9d7ef7..5847820 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs @@ -32,6 +32,7 @@ */ using System; +using System.Linq; using System.Text.Json; using System.Threading.Tasks; using System.Security.Cryptography; @@ -411,37 +412,52 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider isValid = false; } - //Check the audience matches the request uri - if(data.RootElement.TryGetProperty("aud", out JsonElement tokenOriginEl) - && tokenOriginEl.ValueKind == JsonValueKind.String) - { - string? unsafeUserOrigin = tokenOriginEl.GetString(); - string? requestOrigin = null; - - //If strict origin is enabled, we need to check against the request uri - Uri origin = entity.Session.CrossOrigin && _config.EnforceSameOriginToken ? - entity.Session.SpecifiedOrigin! : - (entity.Server.Origin ?? entity.Server.RequestUri); //Finally fall back to the request uri if no origin is specified - - - requestOrigin = origin.GetLeftPart(UriPartial.Authority); - - //Make sure the token href matches the request uri - isValid &= string.Equals(unsafeUserOrigin, requestOrigin, StringComparison.OrdinalIgnoreCase); + if (_config.VerifyOrigin) + { + //Check the audience matches the request uri + if (data.RootElement.TryGetProperty("aud", out JsonElement tokenOriginEl) + && tokenOriginEl.ValueKind == JsonValueKind.String) + { + string? unsafeUserOrigin = tokenOriginEl.GetString(); - if (!isValid) + if(string.IsNullOrWhiteSpace(unsafeUserOrigin)) + { + isValid = false; + } + else if (_config.EnforceSameOriginToken) + { + //enforce strict origin checking + string strictOrigin = entity.Server.RequestUri.GetLeftPart(UriPartial.Authority); + isValid &= string.Equals(unsafeUserOrigin, strictOrigin, StringComparison.OrdinalIgnoreCase); + + if (!isValid) + { + _logger.Debug("Client security OTP JWT origin mismatch from {ip} : strict origin {current} != {token}", + entity.TrustedRemoteIp, + strictOrigin, + unsafeUserOrigin + ); + } + } + else + { + //Verify against allow list + isValid &= _config.AllowedOrigins!.Contains(unsafeUserOrigin, StringComparer.OrdinalIgnoreCase); + + if (!isValid) + { + _logger.Debug("CST origin not allowed {ip} : {token}", + entity.TrustedRemoteIp, + unsafeUserOrigin + ); + } + } + } + else { - _logger.Debug("Client security OTP JWT origin mismatch from {ip} : {current} != {token}", - entity.TrustedRemoteIp, - requestOrigin, - unsafeUserOrigin - ); + isValid = false; } } - else - { - isValid = false; - } //Check the subject (path) matches the request uri if (data.RootElement.TryGetProperty("path", out JsonElement tokenPathEl) @@ -751,6 +767,10 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider .InclusiveBetween((uint)1, uint.MaxValue) .WithMessage("You must specify a valid value for a web session timeout in seconds"); + val.RuleForEach(c => c.AllowedOrigins) + .Matches(@"^https?://[a-z0-9\-\.]+$") + .WithMessage("The allowed origins must be valid http(s) urls"); + return val; } @@ -833,6 +853,18 @@ namespace VNLib.Plugins.Essentials.Accounts.SecurityProvider [JsonPropertyName("strict_origin")] public bool EnforceSameOriginToken { get; set; } = true; + /// <summary> + /// Enable/disable origin verification for the client's token + /// </summary> + [JsonIgnore] + public bool VerifyOrigin => AllowedOrigins != null && AllowedOrigins.Length > 0; + + /// <summary> + /// The list of origins that are allowed to send requests to the server + /// </summary> + [JsonPropertyName("allowed_origins")] + public string[]? AllowedOrigins { get; set; } + void IOnConfigValidation.Validate() { //Validate the current instance diff --git a/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj b/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj index 6faa14d..7d30bc4 100644 --- a/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj +++ b/plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj @@ -59,6 +59,12 @@ <ProjectReference Include="..\..\..\..\Extensions\lib\VNLib.Plugins.Extensions.Validation\src\VNLib.Plugins.Extensions.Validation.csproj" /> </ItemGroup> + <ItemGroup> + <None Update="Essentials.Accounts.json"> + <CopyToOutputDirectory>Always</CopyToOutputDirectory> + </None> + </ItemGroup> + <Target Condition="'$(BuildingInsideVisualStudio)' == true" Name="PostBuild" AfterTargets="PostBuildEvent"> <Exec Command="start xcopy "$(TargetDir)" "$(SolutionDir)devplugins\$(TargetName)" /E /Y /R" /> </Target> |