aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2024-01-21 16:45:46 -0500
committerLibravatar vnugent <public@vaughnnugent.com>2024-01-21 16:45:46 -0500
commitd396d5b58a2be0efa307e0e656efb40fa12c024d (patch)
treed9b6edda5f778450864e674c5d81c83969458554
parent335659f2a3d412aa040fd77d871366dc4d4f8501 (diff)
optional origin check, make config public, and create bundle package
-rw-r--r--.gitignore3
-rw-r--r--ci/bundle/Taskfile.yaml61
-rw-r--r--ci/bundle/package.json10
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Endpoints/PasswordResetEndpoint.cs38
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/Essentials.Accounts.json94
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/SecurityProvider/AccountSecProvider.cs86
-rw-r--r--plugins/VNLib.Plugins.Essentials.Accounts/src/VNLib.Plugins.Essentials.Accounts.csproj6
7 files changed, 251 insertions, 47 deletions
diff --git a/.gitignore b/.gitignore
index 1be9774..8c615dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -476,3 +476,6 @@ FodyWeavers.xsd
*.licenseheader
/plugins/**/*.json
/lib/vnlib.browser/dist
+
+#allow essentails.accounts.json config file to be public
+!/plugins/VNLib.Plugins.Essentials.Accounts/src/essentials.accounts.json
diff --git a/ci/bundle/Taskfile.yaml b/ci/bundle/Taskfile.yaml
new file mode 100644
index 0000000..f049bd5
--- /dev/null
+++ b/ci/bundle/Taskfile.yaml
@@ -0,0 +1,61 @@
+# https://taskfile.dev
+
+#Called by the vnbuild system to produce builds for my website
+#https://www.vaughnnugent.com/resources/software
+
+#This taskfile is designed to create a bundle of essentials plugins ready to use
+
+#The Module.Taskfile will build the plugins, we just need to copy the ones we want to use
+
+version: '3'
+
+vars:
+ PROJ_BUILD_OUT_DIR: 'src/bin/Release/net8.0/Publish/'
+ OUT_FILE_NAME: 'essentials-release'
+
+tasks:
+ postbuild_success:
+ dir: '{{.USER_WORKING_DIR}}'
+ cmds:
+ #clean temp dir
+ - defer: powershell -Command "rm -r temp -Force"
+
+ #make output directories
+ - cmd: powershell -Command "mkdir temp -Force" && powershell -Command "mkdir temp/plugins -Force"
+ ignore_error: true
+ - cmd: powershell -Command "mkdir bin -Force"
+ ignore_error: true
+
+ #copy account's plugin to output directory
+ - task: copy-plugin
+ vars:
+ NAME: 'VNLib.Plugins.Essentials.Accounts'
+ OUT_NAME: 'Essentials.Accounts'
+
+ #copy auth.social plugin to output directory
+ - task: copy-plugin
+ vars:
+ NAME: 'VNLib.Plugins.Essentials.Auth.Social'
+ OUT_NAME: 'Auth.Social'
+
+ #copy content.routing plugin to output directory
+ - task: copy-plugin
+ vars:
+ NAME: 'VNLib.Plugins.Essentials.Content.Routing'
+ OUT_NAME: 'PageRouter'
+
+ #tar temp dir and put in output
+ - cmd: cd temp && tar -czf "../bin/{{.OUT_FILE_NAME}}-{{.BUILD_VERSION}}.tgz" .
+
+ copy-plugin:
+ desc: "copy a single plugin project to its output directory"
+ cmds:
+ - cd '{{.MODULE_DIR}}/plugins' && powershell -Command "cp -Path {{.NAME}}/{{.PROJ_BUILD_OUT_DIR}} -Destination {{.PROJECT_DIR}}/temp/plugins/{{.OUT_NAME}} -Force -Recurse"
+
+ clean:
+ desc: "Cleans all build artifacts"
+ cmds:
+ - cmd: powershell -Command "rm -Recurse temp -Force"
+ ignore_error: true
+ - cmd: powershell -Command "rm -Recurse bin -Force"
+ ignore_error: true \ No newline at end of file
diff --git a/ci/bundle/package.json b/ci/bundle/package.json
new file mode 100644
index 0000000..702e532
--- /dev/null
+++ b/ci/bundle/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "Essentials-Bundle",
+ "version": "0.1.0",
+ "type": "module",
+ "copyright": "Copyright \u00A9 2024 Vaughn Nugent",
+ "author": "Vaughn Nugent",
+ "description": "This package is a bundle of essential website plugins ready to be used for projects most web application projects",
+ "repository": "https://github.com/VnUgE/Simple-Bookmark/tree/master/ci/bundle",
+ "output_dir": "bin"
+} \ No newline at end of file
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 &quot;$(TargetDir)&quot; &quot;$(SolutionDir)devplugins\$(TargetName)&quot; /E /Y /R" />
</Target>