Compare commits
No commits in common. "7cec61b959f99a61422f5fb09be5d1eb0c87ef17" and "d0730e14387760e675df3fb293015ec4e12a7dc0" have entirely different histories.
7cec61b959
...
d0730e1438
@ -1,9 +0,0 @@
|
|||||||
**/bin/
|
|
||||||
**/obj/
|
|
||||||
.vs/
|
|
||||||
TestResults/
|
|
||||||
.git/
|
|
||||||
.repo-tasks/
|
|
||||||
.repo-context/
|
|
||||||
.tasks/
|
|
||||||
.agile/
|
|
||||||
63
.gitignore
vendored
63
.gitignore
vendored
@ -1,24 +1,53 @@
|
|||||||
# Repository orchestration folders (local only)
|
# AgileWebs local orchestration
|
||||||
.repo-tasks/
|
|
||||||
.repo-context/
|
|
||||||
.tasks/
|
.tasks/
|
||||||
.agile/
|
.agile/
|
||||||
|
|
||||||
# .NET build outputs
|
# Build artifacts
|
||||||
**/bin/
|
**/[Bb]in/
|
||||||
**/obj/
|
**/[Oo]bj/
|
||||||
|
/**/out/
|
||||||
|
/**/artifacts/
|
||||||
|
|
||||||
|
# IDE and editor files
|
||||||
.vs/
|
.vs/
|
||||||
TestResults/
|
|
||||||
**/TestResults/
|
|
||||||
*.user
|
|
||||||
*.suo
|
|
||||||
*.rsuser
|
|
||||||
|
|
||||||
# IDE
|
|
||||||
.idea/
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
*.rsuser
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
# Runtime-local artifacts
|
# NuGet
|
||||||
logs/
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
**/packages/*
|
||||||
|
!**/packages/build/
|
||||||
|
|
||||||
|
# Test output
|
||||||
|
**/TestResults/
|
||||||
|
*.trx
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
.env.local
|
logs/
|
||||||
.env.*.local
|
|
||||||
|
# Local environment files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.docker/
|
||||||
|
**/.docker/
|
||||||
|
*.pid
|
||||||
|
docker-compose.override.yml
|
||||||
|
docker-compose.*.override.yml
|
||||||
|
|
||||||
|
# OS files
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
<Project>
|
|
||||||
<PropertyGroup>
|
|
||||||
<Authors>AgileWebs</Authors>
|
|
||||||
<Company>AgileWebs</Company>
|
|
||||||
<RepositoryType>git</RepositoryType>
|
|
||||||
<RepositoryUrl>https://gitea.dream-views.com/AgileWebs/thalos-service</RepositoryUrl>
|
|
||||||
<PackageProjectUrl>https://gitea.dream-views.com/AgileWebs/thalos-service</PackageProjectUrl>
|
|
||||||
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
|
||||||
23
Dockerfile
23
Dockerfile
@ -1,23 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
ARG SDK_IMAGE=mcr.microsoft.com/dotnet/sdk:10.0
|
|
||||||
ARG RUNTIME_IMAGE=mcr.microsoft.com/dotnet/aspnet:10.0
|
|
||||||
|
|
||||||
FROM ${SDK_IMAGE} AS build
|
|
||||||
ARG NUGET_FEED_URL=https://gitea.dream-views.com/api/packages/AgileWebs/nuget/index.json
|
|
||||||
ARG NUGET_FEED_USERNAME=
|
|
||||||
ARG NUGET_FEED_TOKEN=
|
|
||||||
WORKDIR /src
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN if [ -n "$NUGET_FEED_USERNAME" ] && [ -n "$NUGET_FEED_TOKEN" ]; then dotnet nuget add source "$NUGET_FEED_URL" --name gitea-org --username "$NUGET_FEED_USERNAME" --password "$NUGET_FEED_TOKEN" --store-password-in-clear-text --allow-insecure-connections --configfile /root/.nuget/NuGet/NuGet.Config; fi
|
|
||||||
|
|
||||||
RUN dotnet restore "src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj" --configfile /root/.nuget/NuGet/NuGet.Config
|
|
||||||
RUN dotnet publish "src/Thalos.Service.Grpc/Thalos.Service.Grpc.csproj" -c Release -o /app/publish /p:UseAppHost=false --no-restore
|
|
||||||
|
|
||||||
FROM ${RUNTIME_IMAGE} AS runtime
|
|
||||||
WORKDIR /app
|
|
||||||
ENV ASPNETCORE_ENVIRONMENT=Production
|
|
||||||
EXPOSE 8080
|
|
||||||
EXPOSE 8081
|
|
||||||
COPY --from=build /app/publish .
|
|
||||||
ENTRYPOINT ["dotnet", "Thalos.Service.Grpc.dll"]
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
# Thalos Service Contract Package Baseline
|
|
||||||
|
|
||||||
## Feed
|
|
||||||
|
|
||||||
- Source: `https://gitea.dream-views.com/api/packages/AgileWebs/nuget/index.json`
|
|
||||||
- Authentication: Gitea login + token
|
|
||||||
- HTTP requirement: `allowInsecureConnections="true"` in `nuget.config`
|
|
||||||
|
|
||||||
## Published Baseline
|
|
||||||
|
|
||||||
| Package | Version | Published On |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| Thalos.Service.Identity.Abstractions | 0.2.0 | 2026-02-25 |
|
|
||||||
|
|
||||||
## Dependency Note
|
|
||||||
|
|
||||||
`Thalos.Service.Identity.Abstractions` depends on:
|
|
||||||
|
|
||||||
- `Core.Blueprint.Common` version `0.2.0`
|
|
||||||
|
|
||||||
## Consumer Validation
|
|
||||||
|
|
||||||
Restore validation passed using:
|
|
||||||
|
|
||||||
- `TargetFramework`: `net10.0`
|
|
||||||
- `PackageReference`: `Thalos.Service.Identity.Abstractions` `0.2.0`
|
|
||||||
- Restore flags: `--no-cache --force`
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
# Session Runtime Contract
|
|
||||||
|
|
||||||
## Canonical Internal gRPC Operations
|
|
||||||
|
|
||||||
`IdentityRuntime` now exposes the canonical session operations consumed by `thalos-bff`:
|
|
||||||
|
|
||||||
- `StartIdentitySession`
|
|
||||||
- `RefreshIdentitySession`
|
|
||||||
- `IssueIdentityToken` (compatibility)
|
|
||||||
- `EvaluateIdentityPolicy` (policy guardrail)
|
|
||||||
|
|
||||||
## Session Flow
|
|
||||||
|
|
||||||
1. BFF calls `StartIdentitySession` with subject/tenant/provider/external token.
|
|
||||||
2. Service issues access token through existing token orchestration.
|
|
||||||
3. Service generates refresh token through provider-agnostic session token codec.
|
|
||||||
4. BFF calls `RefreshIdentitySession` with refresh token.
|
|
||||||
5. Service validates refresh token signature/expiry and reissues session tokens.
|
|
||||||
|
|
||||||
## Provider-Agnostic Secret Boundary
|
|
||||||
|
|
||||||
Session refresh token signing is bound to `IIdentitySecretMaterialProvider`.
|
|
||||||
|
|
||||||
- Contract is provider-neutral.
|
|
||||||
- Runtime binding is configuration-based by default.
|
|
||||||
- Vault/cloud/env adapters can be swapped at DI boundaries without changing use-case code.
|
|
||||||
|
|
||||||
## Configuration Keys
|
|
||||||
|
|
||||||
- `ThalosIdentity:Secrets:SessionSigning`
|
|
||||||
- `ThalosIdentity:Secrets:Default` (fallback)
|
|
||||||
@ -18,10 +18,3 @@
|
|||||||
- Token issuance and policy evaluation are orchestrated in service use cases.
|
- Token issuance and policy evaluation are orchestrated in service use cases.
|
||||||
- Data retrieval and persistence details remain in thalos-dal and identity adapters.
|
- Data retrieval and persistence details remain in thalos-dal and identity adapters.
|
||||||
- Protocol adaptation remains outside use-case logic.
|
- Protocol adaptation remains outside use-case logic.
|
||||||
|
|
||||||
## Session Extension
|
|
||||||
|
|
||||||
- `IStartIdentitySessionUseCase`: orchestrates canonical session login/start behavior.
|
|
||||||
- `IRefreshIdentitySessionUseCase`: orchestrates canonical session refresh behavior.
|
|
||||||
- Refresh token security is implemented via provider-agnostic `IIdentitySecretMaterialProvider`.
|
|
||||||
- Runtime gRPC session contract details are documented in `docs/identity/session-runtime-contract.md`.
|
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
# Containerization Runbook
|
|
||||||
|
|
||||||
## Image Build
|
|
||||||
|
|
||||||
If the repo consumes internal packages from Gitea, pass feed credentials as build args.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build --build-arg NUGET_FEED_USERNAME=<gitea-login> --build-arg NUGET_FEED_TOKEN=<gitea-token> -t agilewebs/thalos-service:dev .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run --rm -p 8080:8080 --name thalos-service agilewebs/thalos-service:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
## Health Probe
|
|
||||||
|
|
||||||
- Path: `/health`
|
|
||||||
- Fallback path: `/healthz`
|
|
||||||
- Port: `8080`
|
|
||||||
|
|
||||||
## Runtime Notes
|
|
||||||
|
|
||||||
- Exposes internal identity runtime endpoint set and gRPC service.
|
|
||||||
@ -1,13 +1,10 @@
|
|||||||
using Core.Blueprint.Common.DependencyInjection;
|
using Core.Blueprint.Common.DependencyInjection;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
using Thalos.Domain.Decisions;
|
using Thalos.Domain.Decisions;
|
||||||
using Thalos.DAL.DependencyInjection;
|
using Thalos.DAL.DependencyInjection;
|
||||||
using Thalos.Service.Application.Adapters;
|
using Thalos.Service.Application.Adapters;
|
||||||
using Thalos.Service.Application.Ports;
|
using Thalos.Service.Application.Ports;
|
||||||
using Thalos.Service.Application.Secrets;
|
|
||||||
using Thalos.Service.Application.Sessions;
|
|
||||||
using Thalos.Service.Application.UseCases;
|
using Thalos.Service.Application.UseCases;
|
||||||
|
|
||||||
namespace Thalos.Service.Application.DependencyInjection;
|
namespace Thalos.Service.Application.DependencyInjection;
|
||||||
@ -26,21 +23,15 @@ public static class ThalosServiceRuntimeServiceCollectionExtensions
|
|||||||
{
|
{
|
||||||
services.AddBlueprintRuntimeCore();
|
services.AddBlueprintRuntimeCore();
|
||||||
services.AddThalosDalRuntime();
|
services.AddThalosDalRuntime();
|
||||||
services.TryAddSingleton<IConfiguration>(_ =>
|
|
||||||
new ConfigurationBuilder().AddInMemoryCollection().Build());
|
|
||||||
services.TryAddSingleton<IIdentityPolicyDecisionService, IdentityPolicyDecisionService>();
|
services.TryAddSingleton<IIdentityPolicyDecisionService, IdentityPolicyDecisionService>();
|
||||||
services.TryAddSingleton<IIdentityTokenDecisionService, IdentityTokenDecisionService>();
|
services.TryAddSingleton<IIdentityTokenDecisionService, IdentityTokenDecisionService>();
|
||||||
|
|
||||||
services.TryAddSingleton<IIdentityPolicyGrpcContractAdapter, IdentityPolicyGrpcContractAdapter>();
|
services.TryAddSingleton<IIdentityPolicyGrpcContractAdapter, IdentityPolicyGrpcContractAdapter>();
|
||||||
services.TryAddSingleton<IIdentitySecretMaterialProvider, ConfigurationIdentitySecretMaterialProvider>();
|
|
||||||
services.TryAddSingleton<IIdentitySessionTokenCodec, HmacIdentitySessionTokenCodec>();
|
|
||||||
|
|
||||||
services.TryAddSingleton<IIdentityTokenReadPort, IdentityTokenReadPortDalAdapter>();
|
services.TryAddSingleton<IIdentityTokenReadPort, IdentityTokenReadPortDalAdapter>();
|
||||||
services.TryAddSingleton<IIdentityPolicyContextReadPort, IdentityPolicyContextReadPortDalAdapter>();
|
services.TryAddSingleton<IIdentityPolicyContextReadPort, IdentityPolicyContextReadPortDalAdapter>();
|
||||||
|
|
||||||
services.TryAddSingleton<IIssueIdentityTokenUseCase, IssueIdentityTokenUseCase>();
|
services.TryAddSingleton<IIssueIdentityTokenUseCase, IssueIdentityTokenUseCase>();
|
||||||
services.TryAddSingleton<IStartIdentitySessionUseCase, StartIdentitySessionUseCase>();
|
|
||||||
services.TryAddSingleton<IRefreshIdentitySessionUseCase, RefreshIdentitySessionUseCase>();
|
|
||||||
services.TryAddSingleton<IEvaluateIdentityPolicyUseCase, EvaluateIdentityPolicyUseCase>();
|
services.TryAddSingleton<IEvaluateIdentityPolicyUseCase, EvaluateIdentityPolicyUseCase>();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.Secrets;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Configuration-backed secret material provider.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ConfigurationIdentitySecretMaterialProvider(IConfiguration configuration)
|
|
||||||
: IIdentitySecretMaterialProvider
|
|
||||||
{
|
|
||||||
private const string FallbackSecret = "thalos-dev-secret";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string GetSecret(string secretKey)
|
|
||||||
{
|
|
||||||
var scopedKey = $"ThalosIdentity:Secrets:{secretKey}";
|
|
||||||
var scopedSecret = configuration[scopedKey];
|
|
||||||
if (!string.IsNullOrWhiteSpace(scopedSecret))
|
|
||||||
{
|
|
||||||
return scopedSecret;
|
|
||||||
}
|
|
||||||
|
|
||||||
var defaultSecret = configuration["ThalosIdentity:Secrets:Default"];
|
|
||||||
return string.IsNullOrWhiteSpace(defaultSecret) ? FallbackSecret : defaultSecret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
namespace Thalos.Service.Application.Secrets;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Provider-agnostic boundary for resolving identity secret material.
|
|
||||||
/// </summary>
|
|
||||||
public interface IIdentitySecretMaterialProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Resolves secret material for the requested secret key.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="secretKey">Logical secret key.</param>
|
|
||||||
/// <returns>Secret material value.</returns>
|
|
||||||
string GetSecret(string secretKey);
|
|
||||||
}
|
|
||||||
@ -1,123 +0,0 @@
|
|||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
|
||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
using Thalos.Service.Application.Secrets;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.Sessions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// HMAC-based refresh token codec using provider-agnostic secret material.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class HmacIdentitySessionTokenCodec(
|
|
||||||
IIdentitySecretMaterialProvider secretMaterialProvider)
|
|
||||||
: IIdentitySessionTokenCodec
|
|
||||||
{
|
|
||||||
private const string SigningSecretKey = "SessionSigning";
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public string Encode(IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
var payload = string.Join('|',
|
|
||||||
descriptor.SubjectId,
|
|
||||||
descriptor.TenantId,
|
|
||||||
descriptor.Provider,
|
|
||||||
descriptor.ExpiresAtUtc.ToUnixTimeSeconds().ToString());
|
|
||||||
|
|
||||||
var payloadBytes = Encoding.UTF8.GetBytes(payload);
|
|
||||||
var signatureBytes = Sign(payloadBytes);
|
|
||||||
|
|
||||||
return $"{Base64UrlEncode(payloadBytes)}.{Base64UrlEncode(signatureBytes)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool TryDecode(string token, out IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
descriptor = new IdentitySessionDescriptor(string.Empty, string.Empty, IdentityAuthProvider.InternalJwt, DateTimeOffset.MinValue);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parts = token.Split('.', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
||||||
if (parts.Length != 2)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] payloadBytes;
|
|
||||||
byte[] signatureBytes;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
payloadBytes = Base64UrlDecode(parts[0]);
|
|
||||||
signatureBytes = Base64UrlDecode(parts[1]);
|
|
||||||
}
|
|
||||||
catch (FormatException)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedSignature = Sign(payloadBytes);
|
|
||||||
if (!CryptographicOperations.FixedTimeEquals(signatureBytes, expectedSignature))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload = Encoding.UTF8.GetString(payloadBytes);
|
|
||||||
var payloadParts = payload.Split('|', StringSplitOptions.None);
|
|
||||||
if (payloadParts.Length != 4)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Enum.TryParse<IdentityAuthProvider>(payloadParts[2], true, out var provider))
|
|
||||||
{
|
|
||||||
provider = IdentityAuthProvider.InternalJwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!long.TryParse(payloadParts[3], out var expiresUnix))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expiresAt = DateTimeOffset.FromUnixTimeSeconds(expiresUnix);
|
|
||||||
if (expiresAt <= DateTimeOffset.UtcNow)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
descriptor = new IdentitySessionDescriptor(payloadParts[0], payloadParts[1], provider, expiresAt);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] Sign(byte[] payloadBytes)
|
|
||||||
{
|
|
||||||
var secret = secretMaterialProvider.GetSecret(SigningSecretKey);
|
|
||||||
var keyBytes = Encoding.UTF8.GetBytes(secret);
|
|
||||||
using var hmac = new HMACSHA256(keyBytes);
|
|
||||||
return hmac.ComputeHash(payloadBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Base64UrlEncode(byte[] bytes)
|
|
||||||
{
|
|
||||||
return Convert.ToBase64String(bytes)
|
|
||||||
.TrimEnd('=')
|
|
||||||
.Replace('+', '-')
|
|
||||||
.Replace('/', '_');
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] Base64UrlDecode(string text)
|
|
||||||
{
|
|
||||||
var normalized = text
|
|
||||||
.Replace('-', '+')
|
|
||||||
.Replace('_', '/');
|
|
||||||
|
|
||||||
var padding = normalized.Length % 4;
|
|
||||||
if (padding > 0)
|
|
||||||
{
|
|
||||||
normalized = normalized.PadRight(normalized.Length + (4 - padding), '=');
|
|
||||||
}
|
|
||||||
|
|
||||||
return Convert.FromBase64String(normalized);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
namespace Thalos.Service.Application.Sessions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Encodes and decodes refresh token payloads.
|
|
||||||
/// </summary>
|
|
||||||
public interface IIdentitySessionTokenCodec
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Encodes refresh token data into a transport-safe token string.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="descriptor">Session descriptor payload.</param>
|
|
||||||
/// <returns>Encoded refresh token.</returns>
|
|
||||||
string Encode(IdentitySessionDescriptor descriptor);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attempts to decode refresh token payload.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="token">Encoded refresh token.</param>
|
|
||||||
/// <param name="descriptor">Decoded session descriptor when valid.</param>
|
|
||||||
/// <returns>True when token is valid and not expired.</returns>
|
|
||||||
bool TryDecode(string token, out IdentitySessionDescriptor descriptor);
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.Sessions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Internal session descriptor payload used for refresh token encoding/decoding.
|
|
||||||
/// </summary>
|
|
||||||
public sealed record IdentitySessionDescriptor(
|
|
||||||
string SubjectId,
|
|
||||||
string TenantId,
|
|
||||||
IdentityAuthProvider Provider,
|
|
||||||
DateTimeOffset ExpiresAtUtc);
|
|
||||||
@ -6,13 +6,8 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.0" />
|
<ProjectReference Include="..\..\..\building-block-identity\src\BuildingBlock.Identity.Contracts\BuildingBlock.Identity.Contracts.csproj" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
<ProjectReference Include="..\..\..\thalos-domain\src\Thalos.Domain\Thalos.Domain.csproj" />
|
||||||
<PackageReference Include="BuildingBlock.Identity.Contracts" Version="0.2.0" />
|
<ProjectReference Include="..\..\..\thalos-dal\src\Thalos.DAL\Thalos.DAL.csproj" />
|
||||||
<PackageReference Include="Thalos.Domain" Version="0.2.0" />
|
|
||||||
<PackageReference Include="Thalos.DAL" Version="0.2.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\\Thalos.Service.Identity.Abstractions\\Thalos.Service.Identity.Abstractions.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
using Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
using SessionRefreshRequest = Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionRequest;
|
|
||||||
using SessionRefreshResponse = Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionResponse;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UseCases;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines orchestration boundary for session refresh flows.
|
|
||||||
/// </summary>
|
|
||||||
public interface IRefreshIdentitySessionUseCase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Refreshes an existing identity session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Session refresh request contract.</param>
|
|
||||||
/// <returns>Session refresh response contract.</returns>
|
|
||||||
Task<SessionRefreshResponse> HandleAsync(SessionRefreshRequest request);
|
|
||||||
}
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
using Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UseCases;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Defines orchestration boundary for session login/start flows.
|
|
||||||
/// </summary>
|
|
||||||
public interface IStartIdentitySessionUseCase
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Starts a new identity session.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">Session start request contract.</param>
|
|
||||||
/// <returns>Session start response contract.</returns>
|
|
||||||
Task<StartIdentitySessionResponse> HandleAsync(StartIdentitySessionRequest request);
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Requests;
|
|
||||||
using Thalos.Service.Application.Sessions;
|
|
||||||
using Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
using IdentityIssueRequest = BuildingBlock.Identity.Contracts.Requests.IssueIdentityTokenRequest;
|
|
||||||
using SessionRefreshRequest = Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionRequest;
|
|
||||||
using SessionRefreshResponse = Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionResponse;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UseCases;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default orchestration implementation for session refresh.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class RefreshIdentitySessionUseCase(
|
|
||||||
IIssueIdentityTokenUseCase issueIdentityTokenUseCase,
|
|
||||||
IIdentitySessionTokenCodec sessionTokenCodec)
|
|
||||||
: IRefreshIdentitySessionUseCase
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<SessionRefreshResponse> HandleAsync(SessionRefreshRequest request)
|
|
||||||
{
|
|
||||||
if (!sessionTokenCodec.TryDecode(request.RefreshToken, out var descriptor))
|
|
||||||
{
|
|
||||||
return new SessionRefreshResponse(
|
|
||||||
string.Empty,
|
|
||||||
string.Empty,
|
|
||||||
0,
|
|
||||||
string.Empty,
|
|
||||||
string.Empty,
|
|
||||||
request.Provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
var issueRequest = new IdentityIssueRequest(
|
|
||||||
descriptor.SubjectId,
|
|
||||||
descriptor.TenantId,
|
|
||||||
descriptor.Provider);
|
|
||||||
|
|
||||||
var issueResponse = await issueIdentityTokenUseCase.HandleAsync(issueRequest);
|
|
||||||
var refreshToken = sessionTokenCodec.Encode(new IdentitySessionDescriptor(
|
|
||||||
descriptor.SubjectId,
|
|
||||||
descriptor.TenantId,
|
|
||||||
descriptor.Provider,
|
|
||||||
DateTimeOffset.UtcNow.AddHours(8)));
|
|
||||||
|
|
||||||
return new SessionRefreshResponse(
|
|
||||||
issueResponse.Token,
|
|
||||||
refreshToken,
|
|
||||||
Math.Max(0, issueResponse.ExpiresInSeconds),
|
|
||||||
descriptor.SubjectId,
|
|
||||||
descriptor.TenantId,
|
|
||||||
descriptor.Provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,44 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Requests;
|
|
||||||
using Thalos.Service.Application.Sessions;
|
|
||||||
using Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
using IdentityIssueRequest = BuildingBlock.Identity.Contracts.Requests.IssueIdentityTokenRequest;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UseCases;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Default orchestration implementation for session login/start.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class StartIdentitySessionUseCase(
|
|
||||||
IIssueIdentityTokenUseCase issueIdentityTokenUseCase,
|
|
||||||
IIdentitySessionTokenCodec sessionTokenCodec)
|
|
||||||
: IStartIdentitySessionUseCase
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public async Task<StartIdentitySessionResponse> HandleAsync(StartIdentitySessionRequest request)
|
|
||||||
{
|
|
||||||
var issueRequest = new IdentityIssueRequest(
|
|
||||||
request.SubjectId,
|
|
||||||
request.TenantId,
|
|
||||||
request.Provider,
|
|
||||||
request.ExternalToken);
|
|
||||||
|
|
||||||
var issueResponse = await issueIdentityTokenUseCase.HandleAsync(issueRequest);
|
|
||||||
var expiresInSeconds = Math.Max(0, issueResponse.ExpiresInSeconds);
|
|
||||||
|
|
||||||
var refreshDescriptor = new IdentitySessionDescriptor(
|
|
||||||
request.SubjectId,
|
|
||||||
request.TenantId,
|
|
||||||
request.Provider,
|
|
||||||
DateTimeOffset.UtcNow.AddHours(8));
|
|
||||||
|
|
||||||
var refreshToken = sessionTokenCodec.Encode(refreshDescriptor);
|
|
||||||
|
|
||||||
return new StartIdentitySessionResponse(
|
|
||||||
issueResponse.Token,
|
|
||||||
refreshToken,
|
|
||||||
expiresInSeconds,
|
|
||||||
request.SubjectId,
|
|
||||||
request.TenantId,
|
|
||||||
request.Provider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,16 +1,7 @@
|
|||||||
using Thalos.Service.Application.DependencyInjection;
|
using Thalos.Service.Application.DependencyInjection;
|
||||||
using Thalos.Service.Grpc.Services;
|
using Thalos.Service.Grpc.Services;
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
var httpPort = builder.Configuration.GetValue("ThalosService:HttpPort", 8080);
|
|
||||||
var grpcPort = builder.Configuration.GetValue("ThalosService:GrpcPort", 8081);
|
|
||||||
|
|
||||||
builder.WebHost.ConfigureKestrel(options =>
|
|
||||||
{
|
|
||||||
options.ListenAnyIP(httpPort, listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
|
||||||
options.ListenAnyIP(grpcPort, listenOptions => listenOptions.Protocols = HttpProtocols.Http2);
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddGrpc();
|
builder.Services.AddGrpc();
|
||||||
builder.Services.AddHealthChecks();
|
builder.Services.AddHealthChecks();
|
||||||
@ -20,6 +11,5 @@ var app = builder.Build();
|
|||||||
|
|
||||||
app.MapGrpcService<IdentityRuntimeGrpcService>();
|
app.MapGrpcService<IdentityRuntimeGrpcService>();
|
||||||
app.MapHealthChecks("/healthz");
|
app.MapHealthChecks("/healthz");
|
||||||
app.MapHealthChecks("/health");
|
|
||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|||||||
@ -5,44 +5,10 @@ option csharp_namespace = "Thalos.Service.Grpc";
|
|||||||
package thalos.service.grpc;
|
package thalos.service.grpc;
|
||||||
|
|
||||||
service IdentityRuntime {
|
service IdentityRuntime {
|
||||||
rpc StartIdentitySession (StartIdentitySessionGrpcRequest) returns (StartIdentitySessionGrpcResponse);
|
|
||||||
rpc RefreshIdentitySession (RefreshIdentitySessionGrpcRequest) returns (RefreshIdentitySessionGrpcResponse);
|
|
||||||
rpc IssueIdentityToken (IssueIdentityTokenGrpcRequest) returns (IssueIdentityTokenGrpcResponse);
|
rpc IssueIdentityToken (IssueIdentityTokenGrpcRequest) returns (IssueIdentityTokenGrpcResponse);
|
||||||
rpc EvaluateIdentityPolicy (EvaluateIdentityPolicyGrpcRequest) returns (EvaluateIdentityPolicyGrpcResponse);
|
rpc EvaluateIdentityPolicy (EvaluateIdentityPolicyGrpcRequest) returns (EvaluateIdentityPolicyGrpcResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartIdentitySessionGrpcRequest {
|
|
||||||
string subject_id = 1;
|
|
||||||
string tenant_id = 2;
|
|
||||||
string provider = 3;
|
|
||||||
string external_token = 4;
|
|
||||||
string correlation_id = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message StartIdentitySessionGrpcResponse {
|
|
||||||
string access_token = 1;
|
|
||||||
string refresh_token = 2;
|
|
||||||
int32 expires_in_seconds = 3;
|
|
||||||
string subject_id = 4;
|
|
||||||
string tenant_id = 5;
|
|
||||||
string provider = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RefreshIdentitySessionGrpcRequest {
|
|
||||||
string refresh_token = 1;
|
|
||||||
string correlation_id = 2;
|
|
||||||
string provider = 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
message RefreshIdentitySessionGrpcResponse {
|
|
||||||
string access_token = 1;
|
|
||||||
string refresh_token = 2;
|
|
||||||
int32 expires_in_seconds = 3;
|
|
||||||
string subject_id = 4;
|
|
||||||
string tenant_id = 5;
|
|
||||||
string provider = 6;
|
|
||||||
}
|
|
||||||
|
|
||||||
message IssueIdentityTokenGrpcRequest {
|
message IssueIdentityTokenGrpcRequest {
|
||||||
string subject_id = 1;
|
string subject_id = 1;
|
||||||
string tenant_id = 2;
|
string tenant_id = 2;
|
||||||
|
|||||||
@ -11,70 +11,10 @@ namespace Thalos.Service.Grpc.Services;
|
|||||||
/// Internal gRPC endpoint implementation for identity runtime operations.
|
/// Internal gRPC endpoint implementation for identity runtime operations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class IdentityRuntimeGrpcService(
|
public sealed class IdentityRuntimeGrpcService(
|
||||||
IStartIdentitySessionUseCase startIdentitySessionUseCase,
|
|
||||||
IRefreshIdentitySessionUseCase refreshIdentitySessionUseCase,
|
|
||||||
IIssueIdentityTokenUseCase issueIdentityTokenUseCase,
|
IIssueIdentityTokenUseCase issueIdentityTokenUseCase,
|
||||||
IEvaluateIdentityPolicyUseCase evaluateIdentityPolicyUseCase,
|
IEvaluateIdentityPolicyUseCase evaluateIdentityPolicyUseCase,
|
||||||
IIdentityPolicyGrpcContractAdapter grpcContractAdapter) : IdentityRuntime.IdentityRuntimeBase
|
IIdentityPolicyGrpcContractAdapter grpcContractAdapter) : IdentityRuntime.IdentityRuntimeBase
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Starts identity session through service use-case orchestration.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">gRPC session start request.</param>
|
|
||||||
/// <param name="context">gRPC server call context.</param>
|
|
||||||
/// <returns>gRPC session start response.</returns>
|
|
||||||
public override async Task<StartIdentitySessionGrpcResponse> StartIdentitySession(
|
|
||||||
StartIdentitySessionGrpcRequest request,
|
|
||||||
ServerCallContext context)
|
|
||||||
{
|
|
||||||
var useCaseRequest = new Thalos.Service.Identity.Abstractions.Contracts.StartIdentitySessionRequest(
|
|
||||||
request.SubjectId,
|
|
||||||
request.TenantId,
|
|
||||||
ParseProvider(request.Provider),
|
|
||||||
request.ExternalToken,
|
|
||||||
request.CorrelationId);
|
|
||||||
|
|
||||||
var useCaseResponse = await startIdentitySessionUseCase.HandleAsync(useCaseRequest);
|
|
||||||
|
|
||||||
return new StartIdentitySessionGrpcResponse
|
|
||||||
{
|
|
||||||
AccessToken = useCaseResponse.AccessToken,
|
|
||||||
RefreshToken = useCaseResponse.RefreshToken,
|
|
||||||
ExpiresInSeconds = useCaseResponse.ExpiresInSeconds,
|
|
||||||
SubjectId = useCaseResponse.SubjectId,
|
|
||||||
TenantId = useCaseResponse.TenantId,
|
|
||||||
Provider = useCaseResponse.Provider.ToString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Refreshes identity session through service use-case orchestration.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="request">gRPC session refresh request.</param>
|
|
||||||
/// <param name="context">gRPC server call context.</param>
|
|
||||||
/// <returns>gRPC session refresh response.</returns>
|
|
||||||
public override async Task<RefreshIdentitySessionGrpcResponse> RefreshIdentitySession(
|
|
||||||
RefreshIdentitySessionGrpcRequest request,
|
|
||||||
ServerCallContext context)
|
|
||||||
{
|
|
||||||
var useCaseRequest = new Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionRequest(
|
|
||||||
request.RefreshToken,
|
|
||||||
request.CorrelationId,
|
|
||||||
ParseProvider(request.Provider));
|
|
||||||
|
|
||||||
var useCaseResponse = await refreshIdentitySessionUseCase.HandleAsync(useCaseRequest);
|
|
||||||
|
|
||||||
return new RefreshIdentitySessionGrpcResponse
|
|
||||||
{
|
|
||||||
AccessToken = useCaseResponse.AccessToken,
|
|
||||||
RefreshToken = useCaseResponse.RefreshToken,
|
|
||||||
ExpiresInSeconds = useCaseResponse.ExpiresInSeconds,
|
|
||||||
SubjectId = useCaseResponse.SubjectId,
|
|
||||||
TenantId = useCaseResponse.TenantId,
|
|
||||||
Provider = useCaseResponse.Provider.ToString()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Issues identity token through service use-case orchestration.
|
/// Issues identity token through service use-case orchestration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,14 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transport-neutral request contract for session refresh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="RefreshToken">Refresh token value.</param>
|
|
||||||
/// <param name="CorrelationId">Correlation identifier.</param>
|
|
||||||
/// <param name="Provider">Identity provider for the session.</param>
|
|
||||||
public sealed record RefreshIdentitySessionRequest(
|
|
||||||
string RefreshToken,
|
|
||||||
string CorrelationId,
|
|
||||||
IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt);
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transport-neutral response contract for session refresh.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AccessToken">Refreshed access token value.</param>
|
|
||||||
/// <param name="RefreshToken">Refreshed refresh token value.</param>
|
|
||||||
/// <param name="ExpiresInSeconds">Access token expiration in seconds.</param>
|
|
||||||
/// <param name="SubjectId">Identity subject identifier.</param>
|
|
||||||
/// <param name="TenantId">Tenant scope identifier.</param>
|
|
||||||
/// <param name="Provider">Identity provider for the session.</param>
|
|
||||||
public sealed record RefreshIdentitySessionResponse(
|
|
||||||
string AccessToken,
|
|
||||||
string RefreshToken,
|
|
||||||
int ExpiresInSeconds,
|
|
||||||
string SubjectId,
|
|
||||||
string TenantId,
|
|
||||||
IdentityAuthProvider Provider);
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transport-neutral request contract for session login/start.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="SubjectId">Identity subject identifier.</param>
|
|
||||||
/// <param name="TenantId">Tenant scope identifier.</param>
|
|
||||||
/// <param name="Provider">Identity provider for the session.</param>
|
|
||||||
/// <param name="ExternalToken">External provider token when applicable.</param>
|
|
||||||
/// <param name="CorrelationId">Correlation identifier.</param>
|
|
||||||
public sealed record StartIdentitySessionRequest(
|
|
||||||
string SubjectId,
|
|
||||||
string TenantId,
|
|
||||||
IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt,
|
|
||||||
string ExternalToken = "",
|
|
||||||
string CorrelationId = "");
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Transport-neutral response contract for session login/start.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="AccessToken">Issued access token value.</param>
|
|
||||||
/// <param name="RefreshToken">Issued refresh token value.</param>
|
|
||||||
/// <param name="ExpiresInSeconds">Access token expiration in seconds.</param>
|
|
||||||
/// <param name="SubjectId">Identity subject identifier.</param>
|
|
||||||
/// <param name="TenantId">Tenant scope identifier.</param>
|
|
||||||
/// <param name="Provider">Identity provider for the session.</param>
|
|
||||||
public sealed record StartIdentitySessionResponse(
|
|
||||||
string AccessToken,
|
|
||||||
string RefreshToken,
|
|
||||||
int ExpiresInSeconds,
|
|
||||||
string SubjectId,
|
|
||||||
string TenantId,
|
|
||||||
IdentityAuthProvider Provider);
|
|
||||||
@ -5,7 +5,6 @@
|
|||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Core.Blueprint.Common" Version="0.2.0" />
|
<ProjectReference Include="..\..\..\blueprint-platform\src\Core.Blueprint.Common\Core.Blueprint.Common.csproj" />
|
||||||
<PackageReference Include="BuildingBlock.Identity.Contracts" Version="0.2.0" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
using Thalos.Service.Application.Secrets;
|
|
||||||
using Thalos.Service.Application.Sessions;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UnitTests;
|
|
||||||
|
|
||||||
public class HmacIdentitySessionTokenCodecTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void EncodeAndTryDecode_WhenTokenValid_RoundTripsDescriptor()
|
|
||||||
{
|
|
||||||
var codec = new HmacIdentitySessionTokenCodec(new FakeSecretMaterialProvider());
|
|
||||||
var descriptor = new IdentitySessionDescriptor(
|
|
||||||
"user-9",
|
|
||||||
"tenant-9",
|
|
||||||
IdentityAuthProvider.AzureAd,
|
|
||||||
DateTimeOffset.UtcNow.AddMinutes(5));
|
|
||||||
|
|
||||||
var token = codec.Encode(descriptor);
|
|
||||||
var ok = codec.TryDecode(token, out var decoded);
|
|
||||||
|
|
||||||
Assert.True(ok);
|
|
||||||
Assert.Equal("user-9", decoded.SubjectId);
|
|
||||||
Assert.Equal("tenant-9", decoded.TenantId);
|
|
||||||
Assert.Equal(IdentityAuthProvider.AzureAd, decoded.Provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void TryDecode_WhenTokenTampered_ReturnsFalse()
|
|
||||||
{
|
|
||||||
var codec = new HmacIdentitySessionTokenCodec(new FakeSecretMaterialProvider());
|
|
||||||
var descriptor = new IdentitySessionDescriptor(
|
|
||||||
"user-9",
|
|
||||||
"tenant-9",
|
|
||||||
IdentityAuthProvider.InternalJwt,
|
|
||||||
DateTimeOffset.UtcNow.AddMinutes(5));
|
|
||||||
|
|
||||||
var token = codec.Encode(descriptor) + "tamper";
|
|
||||||
|
|
||||||
var ok = codec.TryDecode(token, out _);
|
|
||||||
|
|
||||||
Assert.False(ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeSecretMaterialProvider : IIdentitySecretMaterialProvider
|
|
||||||
{
|
|
||||||
public string GetSecret(string secretKey) => "unit-test-secret";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,78 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
using BuildingBlock.Identity.Contracts.Requests;
|
|
||||||
using BuildingBlock.Identity.Contracts.Responses;
|
|
||||||
using Thalos.Service.Application.Sessions;
|
|
||||||
using Thalos.Service.Application.UseCases;
|
|
||||||
using Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
using IdentityIssueRequest = BuildingBlock.Identity.Contracts.Requests.IssueIdentityTokenRequest;
|
|
||||||
using IdentityIssueResponse = BuildingBlock.Identity.Contracts.Responses.IssueIdentityTokenResponse;
|
|
||||||
using SessionRefreshRequest = Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionRequest;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UnitTests;
|
|
||||||
|
|
||||||
public class RefreshIdentitySessionUseCaseTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task HandleAsync_WhenRefreshTokenValid_ReissuesSessionTokens()
|
|
||||||
{
|
|
||||||
var useCase = new RefreshIdentitySessionUseCase(new FakeIssueUseCase(), new FakeSessionTokenCodec());
|
|
||||||
|
|
||||||
var response = await useCase.HandleAsync(new SessionRefreshRequest("refresh-token", "corr-1", IdentityAuthProvider.Google));
|
|
||||||
|
|
||||||
Assert.Equal("token-new", response.AccessToken);
|
|
||||||
Assert.Equal(3000, response.ExpiresInSeconds);
|
|
||||||
Assert.Equal("google-sub-1", response.SubjectId);
|
|
||||||
Assert.Equal("tenant-2", response.TenantId);
|
|
||||||
Assert.Equal("refresh-google-sub-1-tenant-2", response.RefreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task HandleAsync_WhenRefreshTokenInvalid_ReturnsEmptyPayload()
|
|
||||||
{
|
|
||||||
var useCase = new RefreshIdentitySessionUseCase(new FakeIssueUseCase(), new InvalidSessionTokenCodec());
|
|
||||||
|
|
||||||
var response = await useCase.HandleAsync(new SessionRefreshRequest("bad-token", "corr-2"));
|
|
||||||
|
|
||||||
Assert.Equal(string.Empty, response.AccessToken);
|
|
||||||
Assert.Equal(0, response.ExpiresInSeconds);
|
|
||||||
Assert.Equal(string.Empty, response.RefreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeIssueUseCase : IIssueIdentityTokenUseCase
|
|
||||||
{
|
|
||||||
public Task<IdentityIssueResponse> HandleAsync(IdentityIssueRequest request)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new IdentityIssueResponse("token-new", 3000));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeSessionTokenCodec : IIdentitySessionTokenCodec
|
|
||||||
{
|
|
||||||
public string Encode(IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
return $"refresh-{descriptor.SubjectId}-{descriptor.TenantId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryDecode(string token, out IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
descriptor = new IdentitySessionDescriptor(
|
|
||||||
"google-sub-1",
|
|
||||||
"tenant-2",
|
|
||||||
IdentityAuthProvider.Google,
|
|
||||||
DateTimeOffset.UtcNow.AddHours(1));
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class InvalidSessionTokenCodec : IIdentitySessionTokenCodec
|
|
||||||
{
|
|
||||||
public string Encode(IdentitySessionDescriptor descriptor) => string.Empty;
|
|
||||||
|
|
||||||
public bool TryDecode(string token, out IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
descriptor = new IdentitySessionDescriptor(string.Empty, string.Empty, IdentityAuthProvider.InternalJwt, DateTimeOffset.MinValue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -5,10 +5,6 @@ using Thalos.Service.Application.Adapters;
|
|||||||
using Thalos.Service.Application.DependencyInjection;
|
using Thalos.Service.Application.DependencyInjection;
|
||||||
using Thalos.Service.Application.Grpc;
|
using Thalos.Service.Application.Grpc;
|
||||||
using Thalos.Service.Application.UseCases;
|
using Thalos.Service.Application.UseCases;
|
||||||
using BuildingIssueRequest = BuildingBlock.Identity.Contracts.Requests.IssueIdentityTokenRequest;
|
|
||||||
using BuildingPolicyRequest = BuildingBlock.Identity.Contracts.Requests.EvaluateIdentityPolicyRequest;
|
|
||||||
using StartSessionRequest = Thalos.Service.Identity.Abstractions.Contracts.StartIdentitySessionRequest;
|
|
||||||
using SessionRefreshRequest = Thalos.Service.Identity.Abstractions.Contracts.RefreshIdentitySessionRequest;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UnitTests;
|
namespace Thalos.Service.Application.UnitTests;
|
||||||
|
|
||||||
@ -22,27 +18,15 @@ public class RuntimeWiringTests
|
|||||||
|
|
||||||
using var provider = services.BuildServiceProvider();
|
using var provider = services.BuildServiceProvider();
|
||||||
var issueTokenUseCase = provider.GetRequiredService<IIssueIdentityTokenUseCase>();
|
var issueTokenUseCase = provider.GetRequiredService<IIssueIdentityTokenUseCase>();
|
||||||
var startSessionUseCase = provider.GetRequiredService<IStartIdentitySessionUseCase>();
|
|
||||||
var refreshSessionUseCase = provider.GetRequiredService<IRefreshIdentitySessionUseCase>();
|
|
||||||
var evaluatePolicyUseCase = provider.GetRequiredService<IEvaluateIdentityPolicyUseCase>();
|
var evaluatePolicyUseCase = provider.GetRequiredService<IEvaluateIdentityPolicyUseCase>();
|
||||||
|
|
||||||
var tokenResponse = await issueTokenUseCase.HandleAsync(new BuildingIssueRequest("user-1", "tenant-1"));
|
var tokenResponse = await issueTokenUseCase.HandleAsync(new IssueIdentityTokenRequest("user-1", "tenant-1"));
|
||||||
var policyResponse = await evaluatePolicyUseCase.HandleAsync(
|
var policyResponse = await evaluatePolicyUseCase.HandleAsync(
|
||||||
new BuildingPolicyRequest("user-1", "tenant-1", "identity.token.issue"));
|
new EvaluateIdentityPolicyRequest("user-1", "tenant-1", "identity.token.issue"));
|
||||||
var startSessionResponse = await startSessionUseCase.HandleAsync(
|
|
||||||
new StartSessionRequest("user-1", "tenant-1"));
|
|
||||||
var refreshSessionResponse = await refreshSessionUseCase.HandleAsync(
|
|
||||||
new SessionRefreshRequest(startSessionResponse.RefreshToken, "corr-rt-1"));
|
|
||||||
|
|
||||||
Assert.Equal("user-1:tenant-1:token", tokenResponse.Token);
|
Assert.Equal("user-1:tenant-1:token", tokenResponse.Token);
|
||||||
Assert.Equal(1800, tokenResponse.ExpiresInSeconds);
|
Assert.Equal(1800, tokenResponse.ExpiresInSeconds);
|
||||||
Assert.True(policyResponse.IsAllowed);
|
Assert.True(policyResponse.IsAllowed);
|
||||||
Assert.Equal("user-1", startSessionResponse.SubjectId);
|
|
||||||
Assert.Equal("tenant-1", startSessionResponse.TenantId);
|
|
||||||
Assert.NotEmpty(startSessionResponse.RefreshToken);
|
|
||||||
Assert.Equal("user-1", refreshSessionResponse.SubjectId);
|
|
||||||
Assert.Equal("tenant-1", refreshSessionResponse.TenantId);
|
|
||||||
Assert.NotEmpty(refreshSessionResponse.RefreshToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@ -55,7 +39,7 @@ public class RuntimeWiringTests
|
|||||||
var issueTokenUseCase = provider.GetRequiredService<IIssueIdentityTokenUseCase>();
|
var issueTokenUseCase = provider.GetRequiredService<IIssueIdentityTokenUseCase>();
|
||||||
|
|
||||||
var tokenResponse = await issueTokenUseCase.HandleAsync(
|
var tokenResponse = await issueTokenUseCase.HandleAsync(
|
||||||
new BuildingIssueRequest("missing-user", "tenant-1"));
|
new IssueIdentityTokenRequest("missing-user", "tenant-1"));
|
||||||
|
|
||||||
Assert.Equal(string.Empty, tokenResponse.Token);
|
Assert.Equal(string.Empty, tokenResponse.Token);
|
||||||
Assert.Equal(0, tokenResponse.ExpiresInSeconds);
|
Assert.Equal(0, tokenResponse.ExpiresInSeconds);
|
||||||
@ -71,7 +55,7 @@ public class RuntimeWiringTests
|
|||||||
var issueTokenUseCase = provider.GetRequiredService<IIssueIdentityTokenUseCase>();
|
var issueTokenUseCase = provider.GetRequiredService<IIssueIdentityTokenUseCase>();
|
||||||
|
|
||||||
var tokenResponse = await issueTokenUseCase.HandleAsync(
|
var tokenResponse = await issueTokenUseCase.HandleAsync(
|
||||||
new BuildingIssueRequest(
|
new IssueIdentityTokenRequest(
|
||||||
string.Empty,
|
string.Empty,
|
||||||
"tenant-2",
|
"tenant-2",
|
||||||
IdentityAuthProvider.AzureAd,
|
IdentityAuthProvider.AzureAd,
|
||||||
@ -85,7 +69,7 @@ public class RuntimeWiringTests
|
|||||||
public void IdentityPolicyGrpcContractAdapter_WhenMapped_PreservesValues()
|
public void IdentityPolicyGrpcContractAdapter_WhenMapped_PreservesValues()
|
||||||
{
|
{
|
||||||
var adapter = new IdentityPolicyGrpcContractAdapter();
|
var adapter = new IdentityPolicyGrpcContractAdapter();
|
||||||
var useCaseRequest = new BuildingPolicyRequest("user-2", "tenant-2", "identity.policy.evaluate");
|
var useCaseRequest = new EvaluateIdentityPolicyRequest("user-2", "tenant-2", "identity.policy.evaluate");
|
||||||
|
|
||||||
var grpcContract = adapter.ToGrpc(useCaseRequest);
|
var grpcContract = adapter.ToGrpc(useCaseRequest);
|
||||||
var roundtrip = adapter.FromGrpc(grpcContract);
|
var roundtrip = adapter.FromGrpc(grpcContract);
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
using BuildingBlock.Identity.Contracts.Conventions;
|
|
||||||
using BuildingBlock.Identity.Contracts.Requests;
|
|
||||||
using BuildingBlock.Identity.Contracts.Responses;
|
|
||||||
using Thalos.Service.Application.Sessions;
|
|
||||||
using Thalos.Service.Application.UseCases;
|
|
||||||
using Thalos.Service.Identity.Abstractions.Contracts;
|
|
||||||
using IdentityIssueRequest = BuildingBlock.Identity.Contracts.Requests.IssueIdentityTokenRequest;
|
|
||||||
using IdentityIssueResponse = BuildingBlock.Identity.Contracts.Responses.IssueIdentityTokenResponse;
|
|
||||||
|
|
||||||
namespace Thalos.Service.Application.UnitTests;
|
|
||||||
|
|
||||||
public class StartIdentitySessionUseCaseTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public async Task HandleAsync_WhenCalled_IssuesTokenAndRefreshToken()
|
|
||||||
{
|
|
||||||
var useCase = new StartIdentitySessionUseCase(new FakeIssueUseCase(), new FakeSessionTokenCodec());
|
|
||||||
|
|
||||||
var response = await useCase.HandleAsync(new StartIdentitySessionRequest("user-1", "tenant-1", IdentityAuthProvider.InternalJwt));
|
|
||||||
|
|
||||||
Assert.Equal("token-abc", response.AccessToken);
|
|
||||||
Assert.Equal(1800, response.ExpiresInSeconds);
|
|
||||||
Assert.Equal("user-1", response.SubjectId);
|
|
||||||
Assert.Equal("tenant-1", response.TenantId);
|
|
||||||
Assert.Equal("refresh-user-1-tenant-1", response.RefreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeIssueUseCase : IIssueIdentityTokenUseCase
|
|
||||||
{
|
|
||||||
public Task<IdentityIssueResponse> HandleAsync(IdentityIssueRequest request)
|
|
||||||
{
|
|
||||||
return Task.FromResult(new IdentityIssueResponse("token-abc", 1800));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class FakeSessionTokenCodec : IIdentitySessionTokenCodec
|
|
||||||
{
|
|
||||||
public string Encode(IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
return $"refresh-{descriptor.SubjectId}-{descriptor.TenantId}";
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryDecode(string token, out IdentitySessionDescriptor descriptor)
|
|
||||||
{
|
|
||||||
descriptor = new IdentitySessionDescriptor(string.Empty, string.Empty, IdentityAuthProvider.InternalJwt, DateTimeOffset.UtcNow);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user