merge(development): integrate session flows
This commit is contained in:
commit
cbf38ac9f8
31
docs/identity/session-runtime-contract.md
Normal file
31
docs/identity/session-runtime-contract.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# 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,3 +18,10 @@
|
|||||||
- 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,10 +1,13 @@
|
|||||||
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;
|
||||||
@ -23,15 +26,21 @@ 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;
|
||||||
|
|||||||
@ -0,0 +1,26 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@ -0,0 +1,123 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
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,8 +6,13 @@
|
|||||||
</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" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
|
||||||
<PackageReference Include="BuildingBlock.Identity.Contracts" Version="0.2.0" />
|
<PackageReference Include="BuildingBlock.Identity.Contracts" Version="0.2.0" />
|
||||||
<PackageReference Include="Thalos.Domain" Version="0.2.0" />
|
<PackageReference Include="Thalos.Domain" Version="0.2.0" />
|
||||||
<PackageReference Include="Thalos.DAL" Version="0.2.0" />
|
<PackageReference Include="Thalos.DAL" Version="0.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\\Thalos.Service.Identity.Abstractions\\Thalos.Service.Identity.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,10 +5,44 @@ 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,10 +11,70 @@ 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>
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
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);
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
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);
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
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 = "");
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
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);
|
||||||
@ -6,5 +6,6 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Core.Blueprint.Common" Version="0.2.0" />
|
<PackageReference Include="Core.Blueprint.Common" Version="0.2.0" />
|
||||||
|
<PackageReference Include="BuildingBlock.Identity.Contracts" Version="0.2.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
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,6 +5,10 @@ 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;
|
||||||
|
|
||||||
@ -18,15 +22,27 @@ 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 IssueIdentityTokenRequest("user-1", "tenant-1"));
|
var tokenResponse = await issueTokenUseCase.HandleAsync(new BuildingIssueRequest("user-1", "tenant-1"));
|
||||||
var policyResponse = await evaluatePolicyUseCase.HandleAsync(
|
var policyResponse = await evaluatePolicyUseCase.HandleAsync(
|
||||||
new EvaluateIdentityPolicyRequest("user-1", "tenant-1", "identity.token.issue"));
|
new BuildingPolicyRequest("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]
|
||||||
@ -39,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 IssueIdentityTokenRequest("missing-user", "tenant-1"));
|
new BuildingIssueRequest("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);
|
||||||
@ -55,7 +71,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 IssueIdentityTokenRequest(
|
new BuildingIssueRequest(
|
||||||
string.Empty,
|
string.Empty,
|
||||||
"tenant-2",
|
"tenant-2",
|
||||||
IdentityAuthProvider.AzureAd,
|
IdentityAuthProvider.AzureAd,
|
||||||
@ -69,7 +85,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 EvaluateIdentityPolicyRequest("user-2", "tenant-2", "identity.policy.evaluate");
|
var useCaseRequest = new BuildingPolicyRequest("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);
|
||||||
|
|||||||
@ -0,0 +1,49 @@
|
|||||||
|
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