diff --git a/docs/identity/session-runtime-contract.md b/docs/identity/session-runtime-contract.md index 1a6eb9b..ed535fd 100644 --- a/docs/identity/session-runtime-contract.md +++ b/docs/identity/session-runtime-contract.md @@ -26,6 +26,8 @@ Session refresh token signing is bound to `IIdentitySecretMaterialProvider`. - Runtime binding is configuration-based by default. - Vault/cloud/env adapters can be swapped at DI boundaries without changing use-case code. - OIDC provider material uses the same boundary (no provider SDK coupling in use-case logic). +- Missing secrets fail explicitly at runtime; the service no longer falls back to a baked-in signing secret. +- `AddThalosServiceRuntime()` only provides a local in-memory session-signing default when no host configuration is present, so isolated tests and developer runs stay deterministic without changing production behavior. ## Configuration Keys @@ -33,3 +35,9 @@ Session refresh token signing is bound to `IIdentitySecretMaterialProvider`. - `ThalosIdentity:Secrets:Oidc:Google:ClientId` - `ThalosIdentity:Secrets:Oidc:Google:Issuer` (optional, defaults to `https://accounts.google.com`) - `ThalosIdentity:Secrets:Default` (fallback) + +## Production Expectation + +- Production hosts must provide `ThalosIdentity:Secrets:SessionSigning`. +- Google OIDC validation must provide `ThalosIdentity:Secrets:Oidc:Google:ClientId`. +- The optional `Default` key is intended for non-sensitive shared local/test values, not as a production substitute for explicit signing material. diff --git a/docs/runbooks/containerization.md b/docs/runbooks/containerization.md index 9e18c9d..a41fd0c 100644 --- a/docs/runbooks/containerization.md +++ b/docs/runbooks/containerization.md @@ -28,3 +28,5 @@ docker run --rm -p 8080:8080 \ - Exposes internal identity runtime endpoint set and gRPC service. - Google OIDC claim validation requires `ThalosIdentity:Secrets:Oidc:Google:ClientId`. +- Session refresh signing requires `ThalosIdentity:Secrets:SessionSigning`; there is no baked-in production fallback secret. +- If the host does not provide configuration, `AddThalosServiceRuntime()` supplies a local in-memory session-signing default strictly for isolated tests and developer-only runtime wiring. diff --git a/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs index bb541ba..1742a4e 100644 --- a/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs +++ b/src/Thalos.Service.Application/DependencyInjection/ThalosServiceRuntimeServiceCollectionExtensions.cs @@ -18,6 +18,12 @@ namespace Thalos.Service.Application.DependencyInjection; /// public static class ThalosServiceRuntimeServiceCollectionExtensions { + private static readonly IReadOnlyDictionary LocalRuntimeDefaults = + new Dictionary(StringComparer.Ordinal) + { + ["ThalosIdentity:Secrets:SessionSigning"] = "thalos-local-session-signing-secret" + }; + /// /// Adds thalos-service runtime wiring aligned with blueprint runtime and thalos-dal runtime. /// @@ -28,7 +34,8 @@ public static class ThalosServiceRuntimeServiceCollectionExtensions services.AddBlueprintRuntimeCore(); services.AddThalosDalRuntime(); services.TryAddSingleton(_ => - new ConfigurationBuilder().AddInMemoryCollection().Build()); + // Local-only defaults keep isolated tests and developer runs deterministic. + new ConfigurationBuilder().AddInMemoryCollection(LocalRuntimeDefaults).Build()); services.TryAddSingleton(); services.TryAddSingleton(); diff --git a/src/Thalos.Service.Application/Secrets/ConfigurationIdentitySecretMaterialProvider.cs b/src/Thalos.Service.Application/Secrets/ConfigurationIdentitySecretMaterialProvider.cs index 8ae3dfd..594d563 100644 --- a/src/Thalos.Service.Application/Secrets/ConfigurationIdentitySecretMaterialProvider.cs +++ b/src/Thalos.Service.Application/Secrets/ConfigurationIdentitySecretMaterialProvider.cs @@ -8,8 +8,6 @@ namespace Thalos.Service.Application.Secrets; public sealed class ConfigurationIdentitySecretMaterialProvider(IConfiguration configuration) : IIdentitySecretMaterialProvider { - private const string FallbackSecret = "thalos-dev-secret"; - /// public bool TryGetSecret(string secretKey, out string secretValue) { @@ -39,7 +37,8 @@ public sealed class ConfigurationIdentitySecretMaterialProvider(IConfiguration c { return secretValue; } - - return FallbackSecret; + + throw new InvalidOperationException( + $"Identity secret '{secretKey}' is not configured for the current runtime."); } } diff --git a/src/Thalos.Service.Application/Secrets/IIdentitySecretMaterialProvider.cs b/src/Thalos.Service.Application/Secrets/IIdentitySecretMaterialProvider.cs index d51ff0d..db90579 100644 --- a/src/Thalos.Service.Application/Secrets/IIdentitySecretMaterialProvider.cs +++ b/src/Thalos.Service.Application/Secrets/IIdentitySecretMaterialProvider.cs @@ -18,5 +18,6 @@ public interface IIdentitySecretMaterialProvider /// /// Logical secret key. /// Secret material value. + /// Thrown when the secret is not available in the active runtime. string GetSecret(string secretKey); } diff --git a/tests/Thalos.Service.Application.UnitTests/ConfigurationIdentitySecretMaterialProviderTests.cs b/tests/Thalos.Service.Application.UnitTests/ConfigurationIdentitySecretMaterialProviderTests.cs new file mode 100644 index 0000000..33fbf13 --- /dev/null +++ b/tests/Thalos.Service.Application.UnitTests/ConfigurationIdentitySecretMaterialProviderTests.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.Configuration; +using Thalos.Service.Application.Secrets; + +namespace Thalos.Service.Application.UnitTests; + +public class ConfigurationIdentitySecretMaterialProviderTests +{ + [Fact] + public void TryGetSecret_WhenScopedSecretConfigured_ReturnsScopedValue() + { + var provider = CreateProvider(new Dictionary + { + ["ThalosIdentity:Secrets:SessionSigning"] = "scoped-secret", + ["ThalosIdentity:Secrets:Default"] = "default-secret" + }); + + var ok = provider.TryGetSecret("SessionSigning", out var secretValue); + + Assert.True(ok); + Assert.Equal("scoped-secret", secretValue); + } + + [Fact] + public void TryGetSecret_WhenScopedSecretMissing_UsesDefaultSecret() + { + var provider = CreateProvider(new Dictionary + { + ["ThalosIdentity:Secrets:Default"] = "default-secret" + }); + + var ok = provider.TryGetSecret("MissingSecret", out var secretValue); + + Assert.True(ok); + Assert.Equal("default-secret", secretValue); + } + + [Fact] + public void GetSecret_WhenSecretMissing_ThrowsExplicitRuntimeError() + { + var provider = CreateProvider(new Dictionary()); + + var error = Assert.Throws(() => provider.GetSecret("SessionSigning")); + + Assert.Contains("SessionSigning", error.Message, StringComparison.Ordinal); + } + + private static ConfigurationIdentitySecretMaterialProvider CreateProvider( + IReadOnlyDictionary configurationValues) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(configurationValues) + .Build(); + + return new ConfigurationIdentitySecretMaterialProvider(configuration); + } +} diff --git a/tests/Thalos.Service.Application.UnitTests/HmacIdentitySessionTokenCodecTests.cs b/tests/Thalos.Service.Application.UnitTests/HmacIdentitySessionTokenCodecTests.cs index 654b1be..870852c 100644 --- a/tests/Thalos.Service.Application.UnitTests/HmacIdentitySessionTokenCodecTests.cs +++ b/tests/Thalos.Service.Application.UnitTests/HmacIdentitySessionTokenCodecTests.cs @@ -42,6 +42,21 @@ public class HmacIdentitySessionTokenCodecTests Assert.False(ok); } + [Fact] + public void Encode_WhenSigningSecretUnavailable_ThrowsExplicitRuntimeError() + { + var codec = new HmacIdentitySessionTokenCodec(new MissingSecretMaterialProvider()); + var descriptor = new IdentitySessionDescriptor( + "user-9", + "tenant-9", + IdentityAuthProvider.InternalJwt, + DateTimeOffset.UtcNow.AddMinutes(5)); + + var error = Assert.Throws(() => codec.Encode(descriptor)); + + Assert.Contains("SessionSigning", error.Message, StringComparison.Ordinal); + } + private sealed class FakeSecretMaterialProvider : IIdentitySecretMaterialProvider { public bool TryGetSecret(string secretKey, out string secretValue) @@ -52,4 +67,19 @@ public class HmacIdentitySessionTokenCodecTests public string GetSecret(string secretKey) => "unit-test-secret"; } + + private sealed class MissingSecretMaterialProvider : IIdentitySecretMaterialProvider + { + public bool TryGetSecret(string secretKey, out string secretValue) + { + secretValue = string.Empty; + return false; + } + + public string GetSecret(string secretKey) + { + throw new InvalidOperationException( + $"Identity secret '{secretKey}' is not configured for the current runtime."); + } + } }