From 14a0aaaa9020029ddb17290dfee75a9831302fad Mon Sep 17 00:00:00 2001 From: vivet Date: Thu, 9 Jul 2026 17:57:12 +0200 Subject: [PATCH 1/4] Updated --- Nano.App.Api/README.md | 56 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/Nano.App.Api/README.md b/Nano.App.Api/README.md index 252fb6e4..51b52b72 100644 --- a/Nano.App.Api/README.md +++ b/Nano.App.Api/README.md @@ -1555,14 +1555,66 @@ The following configuration is available for authentication. In a distributed application architecture, the application responsible for signing in users must be configured with both a private and a public key, while all other applications only need the public key. The private key is used to generate JWT tokens, whereas the public key is sufficient to validate them. -Both the public and private keys should be stored securily on GitHub and deployed as Kubernetes secrets for the `Staging` and `Production` environments and should not be exposed. +Both the public and private keys should be stored securily on GitHub and deployed as Kubernetes secrets for the `Staging` and `Production` environments and should not be exposed. | Variable | Type | Description | | ------------------------------------- | -------- | -------------------------------------- | | {{environment}}_AUTH_JWT_PUBLIC_KEY | secrets | The public JWT key. | | {{environment}}_AUTH_JWT_PRIVATE_KEY | secrets | The private JWT key. | -The variables should be created in Kubernetes as a secret and referenced in the deployment, to securely store the public and private keys. +Configure the GitHub Actions workflow for the application responsible for generating JWT tokens by adding the following environment variables. + +```yaml +env: + AUTH_JWT_PUBLIC_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PUBLIC_KEY || secrets.STAGING_AUTH_JWT_PUBLIC_KEY }} + AUTH_JWT_PRIVATE_KEY: ${{ github.ref == 'refs/heads/master' && secrets.PRODUCTION_AUTH_JWT_PRIVATE_KEY || secrets.STAGING_AUTH_JWT_PRIVATE_KEY }} +``` + +Create a Kubernetes secret that stores the JWT public and private keys, allowing them to be securely consumed by the application. + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: auth-jwt-secret + namespace: %KUBERNETES_NAMESPACE% +type: Opaque +stringData: + jwt-public-key: %AUTH_JWT_PUBLIC_KEY% + jwt-private-key: %AUTH_JWT_PRIVATE_KEY% +``` + +During the Kubernetes deployment step in the GitHub Actions workflow, expand the environment variables in the secret manifest and apply the secret to the cluster. + +```powershell +Get-Content .kubernetes/auth-jwt-secret.yaml | foreach { [Environment]::ExpandEnvironmentVariables($_) } | Set-Content .kubernetes/auth-jwt-secret.tmp.yaml; +kubectl apply -f .kubernetes/auth-jwt-secret.tmp.yaml; +if ($LastExitCode -ne 0) +{ + throw "error"; +}; +``` + +Finally, reference the secret in the application `deployment.yaml`. Applications that only validate JWT tokens only need to reference the public key, while applications that +generate JWT tokens should reference both the public and private keys. + +```yaml +spec: + template: + spec: + containers: + env: + - name: App__Authentication__Jwt__PublicKey + valueFrom: + secretKeyRef: + name: auth-jwt-secret + key: jwt-public-key + - name: App__Authentication__Jwt__PrivateKey + valueFrom: + secretKeyRef: + name: auth-jwt-secret + key: jwt-private-key +``` To generate the private and public keys, run the following in a Console application. From 779404c058e60a55f4aaf1044ea7ad829a9a0f27 Mon Sep 17 00:00:00 2001 From: vivet Date: Sun, 12 Jul 2026 15:43:46 +0200 Subject: [PATCH 2/4] Updated --- Nano.App.Api/README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Nano.App.Api/README.md b/Nano.App.Api/README.md index 51b52b72..4effadba 100644 --- a/Nano.App.Api/README.md +++ b/Nano.App.Api/README.md @@ -243,10 +243,9 @@ The `appsettings.Development.json` should have the credentials for the self-sign ```json "App": { "Hosting": { - "Certificate": { - "Path": null, - "Password": null - } + "Certificate": { + "Path": null, + "Password": null } } } @@ -944,9 +943,9 @@ Nano intercepts browser preflight (OPTIONS) requests and responds with the corre | Setting | Type | Default | Description | | ------------------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -| `AllowedOrigins` | array | [] | Allowed origins. | -| `AllowedHeaders` | array | [] | Allowed HTTP headers. | -| `AllowedMethods` | array | [] | Allowed HTTP methods. | +| `AllowedOrigins` | array | [] | Allowed origins. Empty array means allowing all. | +| `AllowedHeaders` | array | [] | Allowed HTTP headers. Empty array means allowing all. | +| `AllowedMethods` | array | [] | Allowed HTTP methods. Empty array means allowing all. | | `AllowCredentials` | bool | false | Indicates whether credentials are allowed. | | `Origin` | object | default | Origin-specific CORS policies. | | `Origin.EmbedderPolicy` | object | default | The HTTP Cross-Origin-Embedder-Policy (COEP) response header configures the current document's policy for loading and embedding cross-origin resources. Allowed values: `UnsafeNone`⭐, `RequireCorp` or `Credentialless`. | From 0cb265979e19f08d20217d6928764250c8c83a02 Mon Sep 17 00:00:00 2001 From: vivet Date: Mon, 13 Jul 2026 11:59:14 +0200 Subject: [PATCH 3/4] Updated --- .../Extensions/ServiceCollectionExtensions.cs | 4 --- Nano.Common/Config/ConfigManager.cs | 30 ++++++++++++++----- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/Nano.App.Api/Extensions/ServiceCollectionExtensions.cs b/Nano.App.Api/Extensions/ServiceCollectionExtensions.cs index 50d943dd..767fdbdd 100644 --- a/Nano.App.Api/Extensions/ServiceCollectionExtensions.cs +++ b/Nano.App.Api/Extensions/ServiceCollectionExtensions.cs @@ -422,10 +422,6 @@ internal static IServiceCollection AddNanoHttpsRedirection(this IServiceCollecti .FirstOrDefault(); }); - - services - .AddScoped(); - return services; } diff --git a/Nano.Common/Config/ConfigManager.cs b/Nano.Common/Config/ConfigManager.cs index 2e557deb..927fb735 100644 --- a/Nano.Common/Config/ConfigManager.cs +++ b/Nano.Common/Config/ConfigManager.cs @@ -106,11 +106,14 @@ private static void RemoveNulls(JObject baseJson, JObject overrideJson) { var overrideValue = property.Value; + var existingProperty = baseJson + .Property(property.Name, StringComparison.OrdinalIgnoreCase); + if (overrideValue.Type == JTokenType.Null) { - baseJson - .Remove(property.Name); - + existingProperty? + .Remove(); + continue; } @@ -118,15 +121,19 @@ private static void RemoveNulls(JObject baseJson, JObject overrideJson) { if (!overrideObj.HasValues) { - baseJson[property.Name] = new JObject + if (existingProperty != null) { - ["__empty"] = true - }; + existingProperty.Value = new JObject { ["__empty"] = true }; + } + else + { + baseJson[property.Name] = new JObject { ["__empty"] = true }; + } continue; } - if (baseJson[property.Name] is JObject baseObj) + if (existingProperty?.Value is JObject baseObj) { RemoveNulls(baseObj, overrideObj); @@ -134,7 +141,14 @@ private static void RemoveNulls(JObject baseJson, JObject overrideJson) } } - baseJson[property.Name] = overrideValue.DeepClone(); + if (existingProperty != null) + { + existingProperty.Value = overrideValue.DeepClone(); + } + else + { + baseJson[property.Name] = overrideValue.DeepClone(); + } } } } From 9a523f0108bc3d9ec03412dffc64767e731814a0 Mon Sep 17 00:00:00 2001 From: vivet Date: Mon, 13 Jul 2026 18:04:55 +0200 Subject: [PATCH 4/4] Updated --- .github/workflows/build-and-deploy.yml | 2 +- Nano.App.Api/Mvc/Csp/Directives/CspDirectiveChildren.cs | 2 +- Nano.App.Api/Mvc/Csp/Directives/CspDirectiveConnection.cs | 2 +- .../Mvc/Csp/Directives/CspDirectiveFrameAncestors.cs | 2 +- Nano.App.Api/Mvc/Extensions/HttpResponseExtensions.cs | 6 +++--- Nano.App.Api/README.md | 2 +- Nano.App/README.md | 2 ++ Nano.Common/TypeCache.cs | 7 ++++--- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 0729f08f..df62ee8f 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -8,7 +8,7 @@ on: - master env: APP_NAME: Nano.Library - VERSION: 10.0.0-rc3 + VERSION: 10.0.0-rc4 jobs: build-and-deploy: runs-on: windows-latest diff --git a/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveChildren.cs b/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveChildren.cs index 13245b46..3776ef8c 100644 --- a/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveChildren.cs +++ b/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveChildren.cs @@ -2,5 +2,5 @@ namespace Nano.App.Api.Mvc.Csp.Directives; internal class CspDirectiveChildren : BaseCspDirectiveSimple { - public override string Name => "children-src"; + public override string Name => "child-src"; } \ No newline at end of file diff --git a/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveConnection.cs b/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveConnection.cs index 9c6bfc3a..413f3f14 100644 --- a/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveConnection.cs +++ b/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveConnection.cs @@ -2,5 +2,5 @@ namespace Nano.App.Api.Mvc.Csp.Directives; internal class CspDirectiveConnection : BaseCspDirectiveSimple { - public override string Name => "connection-src"; + public override string Name => "connect-src"; } \ No newline at end of file diff --git a/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveFrameAncestors.cs b/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveFrameAncestors.cs index 6a79177c..cee5e7e9 100644 --- a/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveFrameAncestors.cs +++ b/Nano.App.Api/Mvc/Csp/Directives/CspDirectiveFrameAncestors.cs @@ -2,5 +2,5 @@ namespace Nano.App.Api.Mvc.Csp.Directives; internal class CspDirectiveFrameAncestors : BaseCspDirectiveSimple { - public override string Name => "frame-ancestors-src"; + public override string Name => "frame-ancestors"; } \ No newline at end of file diff --git a/Nano.App.Api/Mvc/Extensions/HttpResponseExtensions.cs b/Nano.App.Api/Mvc/Extensions/HttpResponseExtensions.cs index c815e9aa..135f3231 100644 --- a/Nano.App.Api/Mvc/Extensions/HttpResponseExtensions.cs +++ b/Nano.App.Api/Mvc/Extensions/HttpResponseExtensions.cs @@ -402,20 +402,20 @@ internal static HttpResponse AddContentSecurityPolicyPermissionsHeader(this Http permissionPolicyValues += UseCspPermissionsPolicyDirective("identity-credentials-get", cspDirectivePermissionsPolicy.IdentityCredentialsGet); permissionPolicyValues += UseCspPermissionsPolicyDirective("idle-detection", cspDirectivePermissionsPolicy.IdleDetection); permissionPolicyValues += UseCspPermissionsPolicyDirective("language-detector", cspDirectivePermissionsPolicy.LanguageDetector); - permissionPolicyValues += UseCspPermissionsPolicyDirective("local-fonts ", cspDirectivePermissionsPolicy.LocalFonts); + permissionPolicyValues += UseCspPermissionsPolicyDirective("local-fonts", cspDirectivePermissionsPolicy.LocalFonts); permissionPolicyValues += UseCspPermissionsPolicyDirective("layout-animations", cspDirectivePermissionsPolicy.LayoutAnimations); permissionPolicyValues += UseCspPermissionsPolicyDirective("legacy-image-formats", cspDirectivePermissionsPolicy.LegacyImageFormats); permissionPolicyValues += UseCspPermissionsPolicyDirective("magnetometer", cspDirectivePermissionsPolicy.Magnetometer); permissionPolicyValues += UseCspPermissionsPolicyDirective("microphone", cspDirectivePermissionsPolicy.Microphone); permissionPolicyValues += UseCspPermissionsPolicyDirective("midi", cspDirectivePermissionsPolicy.Midi); permissionPolicyValues += UseCspPermissionsPolicyDirective("on-device-speech-recognition", cspDirectivePermissionsPolicy.OnDeviceSpeechRecognition); - permissionPolicyValues += UseCspPermissionsPolicyDirective("otp-credentials ", cspDirectivePermissionsPolicy.OtpCredentials); + permissionPolicyValues += UseCspPermissionsPolicyDirective("otp-credentials", cspDirectivePermissionsPolicy.OtpCredentials); permissionPolicyValues += UseCspPermissionsPolicyDirective("navigation-override", cspDirectivePermissionsPolicy.NavigationOverride); permissionPolicyValues += UseCspPermissionsPolicyDirective("oversized-images", cspDirectivePermissionsPolicy.OversizedImages); permissionPolicyValues += UseCspPermissionsPolicyDirective("payment", cspDirectivePermissionsPolicy.Payment); permissionPolicyValues += UseCspPermissionsPolicyDirective("picture-in-picture", cspDirectivePermissionsPolicy.PictureInPicture); permissionPolicyValues += UseCspPermissionsPolicyDirective("private-state-token-issuance", cspDirectivePermissionsPolicy.PrivateStateTokenIssuance); - permissionPolicyValues += UseCspPermissionsPolicyDirective("private-state-token-redemption ", cspDirectivePermissionsPolicy.PrivateStateTokenRedemption); + permissionPolicyValues += UseCspPermissionsPolicyDirective("private-state-token-redemption", cspDirectivePermissionsPolicy.PrivateStateTokenRedemption); permissionPolicyValues += UseCspPermissionsPolicyDirective("publickey-credentials-create", cspDirectivePermissionsPolicy.PublickeyCredentialsCreate); permissionPolicyValues += UseCspPermissionsPolicyDirective("publickey-credentials-get", cspDirectivePermissionsPolicy.PublicKeyCredentialsGet); permissionPolicyValues += UseCspPermissionsPolicyDirective("screen-wake-lock", cspDirectivePermissionsPolicy.ScreenWakeLock); diff --git a/Nano.App.Api/README.md b/Nano.App.Api/README.md index 4effadba..6fdaacc9 100644 --- a/Nano.App.Api/README.md +++ b/Nano.App.Api/README.md @@ -478,7 +478,7 @@ implement a strong Content-Security-Policy that disables the use of inline JavaS Try it out yourself using the **[Api.PolicyHeaders.XssProtection](https://github.com/Nano-Core/Nano.Lessons/tree/master/Api.PolicyHeaders.XssProtection)** example. -## Content Security Policy +## Content Security Policy (CSP) The HTTP Content-Security-Policy response header allows website administrators to control resources the user agent is allowed to load for a given page. With a few exceptions, policies mostly involve specifying server origins and script endpoints. This helps guard against cross-site scripting attacks. diff --git a/Nano.App/README.md b/Nano.App/README.md index 46ba8dd0..852ba8ec 100644 --- a/Nano.App/README.md +++ b/Nano.App/README.md @@ -153,6 +153,8 @@ to avoid conflicts. All API client implementations are automatically registered during startup using the options defined in the configuration. They can then be injected and used wherever needed through dependency injection. +> ⚠️ Nano only registers API clients that are actually used. If an API client is configured but never injected or referenced by the application, it will not be registered. + The application has access to several groups of endpoints exposed as properties on the `BaseApiClient`: Entity, Auth, and Audit. Implementations deriving from `BaseIdentityApiClient` also have access to the Identity group. Each group contains endpoints organized by their respective domain and purpose. Endpoints that are not enabled in the API application via configuration will return a 404 response if invoked. For example, if authentication is disabled and an endpoint from the Auth group is called, the request will result in a 404 response. diff --git a/Nano.Common/TypeCache.cs b/Nano.Common/TypeCache.cs index 15fe244a..bb32cebd 100644 --- a/Nano.Common/TypeCache.cs +++ b/Nano.Common/TypeCache.cs @@ -64,9 +64,9 @@ private static HashSet LoadAllReferencedAssemblies() LoadRecursive(entry, assemblies); } - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { - LoadRecursive(asm, assemblies); + LoadRecursive(assembly, assemblies); } return assemblies; @@ -85,7 +85,8 @@ private static void LoadRecursive(Assembly assembly, ISet hashSet) try { - references = assembly.GetReferencedAssemblies(); + references = assembly + .GetReferencedAssemblies(); } catch {