Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions Nano.App.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -422,10 +422,6 @@ internal static IServiceCollection AddNanoHttpsRedirection(this IServiceCollecti
.FirstOrDefault();
});


services
.AddScoped<ExceptionHandlingMiddleware>();

return services;
}

Expand Down
2 changes: 1 addition & 1 deletion Nano.App.Api/Mvc/Csp/Directives/CspDirectiveChildren.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
2 changes: 1 addition & 1 deletion Nano.App.Api/Mvc/Csp/Directives/CspDirectiveConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
6 changes: 3 additions & 3 deletions Nano.App.Api/Mvc/Extensions/HttpResponseExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
71 changes: 61 additions & 10 deletions Nano.App.Api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down Expand Up @@ -479,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.

Expand Down Expand Up @@ -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`. |
Expand Down Expand Up @@ -1555,14 +1554,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.

Expand Down
2 changes: 2 additions & 0 deletions Nano.App/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 22 additions & 8 deletions Nano.Common/Config/ConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,35 +106,49 @@ 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;
}

if (overrideValue is JObject overrideObj)
{
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);

continue;
}
}

baseJson[property.Name] = overrideValue.DeepClone();
if (existingProperty != null)
{
existingProperty.Value = overrideValue.DeepClone();
}
else
{
baseJson[property.Name] = overrideValue.DeepClone();
}
}
}
}
7 changes: 4 additions & 3 deletions Nano.Common/TypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ private static HashSet<Assembly> 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;
Expand All @@ -85,7 +85,8 @@ private static void LoadRecursive(Assembly assembly, ISet<Assembly> hashSet)

try
{
references = assembly.GetReferencedAssemblies();
references = assembly
.GetReferencedAssemblies();
}
catch
{
Expand Down
Loading