diff --git a/docs/api/identity-edge-api.md b/docs/api/identity-edge-api.md index 0423fa2..822b38a 100644 --- a/docs/api/identity-edge-api.md +++ b/docs/api/identity-edge-api.md @@ -7,13 +7,22 @@ ## Entrypoints -- `POST /api/identity/token` -- `POST /api/identity/session/refresh` +- Canonical session endpoints: + - `POST /api/identity/session/login` + - `POST /api/identity/session/refresh` + - `POST /api/identity/session/logout` + - `GET /api/identity/session/me` +- Compatibility endpoint: + - `POST /api/identity/token` + - `POST /api/identity/login` + - `POST /api/identity/token/refresh` + - `POST /api/identity/logout` ## Boundary Notes - Endpoint handlers perform edge validation and permission checks. -- Token issuance and policy evaluation requests are mapped to thalos-service identity contracts. -- Session refresh requests are mapped through edge contract adapters before downstream calls. +- Session login and refresh call canonical thalos-service session gRPC operations. +- Session cookies are managed at the BFF edge (`thalos_session`, `thalos_refresh`) with env-driven secure flag. +- Token issuance and policy evaluation contracts remain available for compatibility calls. - Business orchestration remains in thalos-service. - Identity abstractions remain owned by Thalos repositories. diff --git a/docs/security/permission-enforcement-map.md b/docs/security/permission-enforcement-map.md index d72d296..a0ca4d2 100644 --- a/docs/security/permission-enforcement-map.md +++ b/docs/security/permission-enforcement-map.md @@ -9,3 +9,7 @@ - Permission checks happen at BFF entrypoints using thalos-service policy responses. - Authorization decisions are explicit and traceable at edge boundaries. +- Auth failure payload shape is standardized as `{ code, message, correlationId }`. +- HTTP semantics: + - `401`: no valid session or failed session issuance/refresh. + - `403`: authenticated but denied by permission policy. diff --git a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs index 2e33aa6..bae766f 100644 --- a/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs +++ b/src/Thalos.Bff.Application/Adapters/IThalosServiceClient.cs @@ -1,5 +1,6 @@ using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Responses; +using Thalos.Bff.Application.Sessions; namespace Thalos.Bff.Application.Adapters; @@ -8,6 +9,14 @@ namespace Thalos.Bff.Application.Adapters; /// public interface IThalosServiceClient { + /// + /// Starts canonical session flow in thalos-service. + /// + /// Identity token issuance request. + /// Request correlation identifier. + /// Session token bundle. + Task StartSessionAsync(IssueIdentityTokenRequest request, string correlationId); + /// /// Requests token issuance from thalos-service. /// @@ -28,4 +37,11 @@ public interface IThalosServiceClient /// Session refresh request. /// Session refresh response. Task RefreshSessionAsync(RefreshIdentitySessionRequest request); + + /// + /// Refreshes canonical session flow in thalos-service. + /// + /// Session refresh request. + /// Session token bundle. + Task RefreshSessionTokensAsync(RefreshIdentitySessionRequest request); } diff --git a/src/Thalos.Bff.Application/Sessions/SessionTokensResult.cs b/src/Thalos.Bff.Application/Sessions/SessionTokensResult.cs new file mode 100644 index 0000000..8dc4a4c --- /dev/null +++ b/src/Thalos.Bff.Application/Sessions/SessionTokensResult.cs @@ -0,0 +1,14 @@ +using BuildingBlock.Identity.Contracts.Conventions; + +namespace Thalos.Bff.Application.Sessions; + +/// +/// Session token payload returned by thalos-service session operations. +/// +public sealed record SessionTokensResult( + string AccessToken, + string RefreshToken, + int ExpiresInSeconds, + string SubjectId, + string TenantId, + IdentityAuthProvider Provider); diff --git a/src/Thalos.Bff.Contracts/Api/ApiErrorResponse.cs b/src/Thalos.Bff.Contracts/Api/ApiErrorResponse.cs new file mode 100644 index 0000000..810c29f --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/ApiErrorResponse.cs @@ -0,0 +1,9 @@ +namespace Thalos.Bff.Contracts.Api; + +/// +/// Standardized API error payload. +/// +/// Stable machine-readable error code. +/// Human-readable error message. +/// Request correlation identifier. +public sealed record ApiErrorResponse(string Code, string Message, string CorrelationId); diff --git a/src/Thalos.Bff.Contracts/Api/SessionLoginApiRequest.cs b/src/Thalos.Bff.Contracts/Api/SessionLoginApiRequest.cs new file mode 100644 index 0000000..3aaace1 --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/SessionLoginApiRequest.cs @@ -0,0 +1,18 @@ +using BuildingBlock.Identity.Contracts.Conventions; + +namespace Thalos.Bff.Contracts.Api; + +/// +/// Canonical API request for session login. +/// +/// Identity subject identifier. +/// Tenant identifier. +/// Request correlation identifier. +/// Identity auth provider. +/// External provider token when applicable. +public sealed record SessionLoginApiRequest( + string SubjectId, + string TenantId, + string CorrelationId, + IdentityAuthProvider Provider = IdentityAuthProvider.InternalJwt, + string ExternalToken = ""); diff --git a/src/Thalos.Bff.Contracts/Api/SessionLoginApiResponse.cs b/src/Thalos.Bff.Contracts/Api/SessionLoginApiResponse.cs new file mode 100644 index 0000000..368380e --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/SessionLoginApiResponse.cs @@ -0,0 +1,16 @@ +using BuildingBlock.Identity.Contracts.Conventions; + +namespace Thalos.Bff.Contracts.Api; + +/// +/// Canonical API response for session login and refresh. +/// +/// Identity subject identifier. +/// Tenant identifier. +/// Identity auth provider. +/// Access token expiration in seconds. +public sealed record SessionLoginApiResponse( + string SubjectId, + string TenantId, + IdentityAuthProvider Provider, + int ExpiresInSeconds); diff --git a/src/Thalos.Bff.Contracts/Api/SessionMeApiResponse.cs b/src/Thalos.Bff.Contracts/Api/SessionMeApiResponse.cs new file mode 100644 index 0000000..833a7ab --- /dev/null +++ b/src/Thalos.Bff.Contracts/Api/SessionMeApiResponse.cs @@ -0,0 +1,16 @@ +using BuildingBlock.Identity.Contracts.Conventions; + +namespace Thalos.Bff.Contracts.Api; + +/// +/// API response contract for current authenticated session details. +/// +/// Indicates whether the caller has an authenticated session. +/// Identity subject identifier. +/// Tenant identifier. +/// Identity auth provider. +public sealed record SessionMeApiResponse( + bool IsAuthenticated, + string SubjectId, + string TenantId, + IdentityAuthProvider Provider); diff --git a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs index 01f3724..5fa5d99 100644 --- a/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs +++ b/src/Thalos.Bff.Rest/Adapters/ThalosServiceGrpcClientAdapter.cs @@ -1,6 +1,7 @@ using Grpc.Core; using Microsoft.Extensions.Primitives; using Thalos.Bff.Application.Adapters; +using Thalos.Bff.Application.Sessions; using Thalos.Service.Grpc; using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Responses; @@ -12,11 +13,35 @@ namespace Thalos.Bff.Rest.Adapters; /// public sealed class ThalosServiceGrpcClientAdapter( IdentityRuntime.IdentityRuntimeClient grpcClient, - IHttpContextAccessor httpContextAccessor, - IConfiguration configuration) : IThalosServiceClient + IHttpContextAccessor httpContextAccessor) : IThalosServiceClient { private const string CorrelationHeaderName = "x-correlation-id"; - private readonly string refreshTenantId = configuration["ThalosService:RefreshTenantId"] ?? "refresh"; + + /// + public async Task StartSessionAsync(IssueIdentityTokenRequest request, string correlationId) + { + var resolvedCorrelationId = ResolveCorrelationId(correlationId); + var grpcRequest = new StartIdentitySessionGrpcRequest + { + SubjectId = request.SubjectId, + TenantId = request.TenantId, + Provider = request.Provider.ToString(), + ExternalToken = request.ExternalToken, + CorrelationId = resolvedCorrelationId + }; + + var grpcResponse = await grpcClient.StartIdentitySessionAsync( + grpcRequest, + headers: CreateHeaders(resolvedCorrelationId)); + + return new SessionTokensResult( + grpcResponse.AccessToken, + grpcResponse.RefreshToken, + grpcResponse.ExpiresInSeconds, + grpcResponse.SubjectId, + grpcResponse.TenantId, + ParseProvider(grpcResponse.Provider)); + } /// public async Task IssueTokenAsync(IssueIdentityTokenRequest request) @@ -61,20 +86,33 @@ public sealed class ThalosServiceGrpcClientAdapter( /// public async Task RefreshSessionAsync(RefreshIdentitySessionRequest request) + { + var sessionTokens = await RefreshSessionTokensAsync(request); + return new RefreshIdentitySessionResponse(sessionTokens.AccessToken, sessionTokens.ExpiresInSeconds); + } + + /// + public async Task RefreshSessionTokensAsync(RefreshIdentitySessionRequest request) { var correlationId = ResolveCorrelationId(request.CorrelationId); - var grpcRequest = new IssueIdentityTokenGrpcRequest + var grpcRequest = new RefreshIdentitySessionGrpcRequest { - SubjectId = request.RefreshToken, - TenantId = refreshTenantId, + RefreshToken = request.RefreshToken, + CorrelationId = correlationId, Provider = request.Provider.ToString() }; - var grpcResponse = await grpcClient.IssueIdentityTokenAsync( + var grpcResponse = await grpcClient.RefreshIdentitySessionAsync( grpcRequest, headers: CreateHeaders(correlationId)); - return new RefreshIdentitySessionResponse(grpcResponse.Token, grpcResponse.ExpiresInSeconds); + return new SessionTokensResult( + grpcResponse.AccessToken, + string.IsNullOrWhiteSpace(grpcResponse.RefreshToken) ? request.RefreshToken : grpcResponse.RefreshToken, + grpcResponse.ExpiresInSeconds, + grpcResponse.SubjectId, + grpcResponse.TenantId, + ParseProvider(grpcResponse.Provider)); } private string ResolveCorrelationId(string? preferred = null) @@ -108,4 +146,11 @@ public sealed class ThalosServiceGrpcClientAdapter( { CorrelationHeaderName, correlationId } }; } + + private static BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider ParseProvider(string provider) + { + return Enum.TryParse(provider, true, out var parsedProvider) + ? parsedProvider + : BuildingBlock.Identity.Contracts.Conventions.IdentityAuthProvider.InternalJwt; + } } diff --git a/src/Thalos.Bff.Rest/Program.cs b/src/Thalos.Bff.Rest/Program.cs index b26c0ec..e014496 100644 --- a/src/Thalos.Bff.Rest/Program.cs +++ b/src/Thalos.Bff.Rest/Program.cs @@ -1,14 +1,20 @@ +using BuildingBlock.Identity.Contracts.Conventions; +using BuildingBlock.Identity.Contracts.Requests; using Core.Blueprint.Common.DependencyInjection; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Thalos.Bff.Application.Adapters; using Thalos.Bff.Application.DependencyInjection; using Thalos.Bff.Application.Handlers; +using Thalos.Bff.Application.Security; using Thalos.Bff.Contracts.Api; using Thalos.Bff.Rest.Adapters; using Thalos.Bff.Rest.Endpoints; using Thalos.Service.Grpc; const string CorrelationHeaderName = "x-correlation-id"; +const string SessionAccessCookieName = "thalos_session"; +const string SessionRefreshCookieName = "thalos_refresh"; var builder = WebApplication.CreateBuilder(args); var edgeProtocol = builder.Configuration["ThalosBff:EdgeProtocol"] ?? "rest"; @@ -40,6 +46,90 @@ app.Use(async (context, next) => await next(); }); +app.MapPost($"{EndpointConventions.ApiPrefix}/session/login", async ( + SessionLoginApiRequest request, + HttpContext context, + IThalosServiceClient serviceClient, + IIdentityEdgeContractAdapter contractAdapter, + IPermissionGuard permissionGuard) => +{ + var correlationId = ResolveCorrelationId(context, request.CorrelationId); + var issueRequest = new IssueTokenApiRequest( + request.SubjectId, + request.TenantId, + correlationId, + request.Provider, + request.ExternalToken); + + var policyRequest = contractAdapter.ToPolicyRequest(issueRequest, "identity.token.issue"); + var policyResponse = await serviceClient.EvaluatePolicyAsync(policyRequest); + if (!permissionGuard.CanAccess(policyResponse)) + { + return ErrorResult(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId); + } + + var serviceRequest = contractAdapter.ToIssueTokenRequest(issueRequest); + var sessionTokens = await serviceClient.StartSessionAsync(serviceRequest, correlationId); + + if (string.IsNullOrWhiteSpace(sessionTokens.AccessToken) || string.IsNullOrWhiteSpace(sessionTokens.RefreshToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_login_failed", "Unable to issue session.", correlationId); + } + + WriteSessionCookies(context, sessionTokens, builder.Configuration); + + var response = new SessionLoginApiResponse( + sessionTokens.SubjectId, + sessionTokens.TenantId, + sessionTokens.Provider, + sessionTokens.ExpiresInSeconds); + + return Results.Ok(response); +}); + +// Compatibility alias kept for existing token-based callers. +app.MapPost($"{EndpointConventions.ApiPrefix}/login", async ( + SessionLoginApiRequest request, + HttpContext context, + IThalosServiceClient serviceClient, + IIdentityEdgeContractAdapter contractAdapter, + IPermissionGuard permissionGuard) => +{ + var correlationId = ResolveCorrelationId(context, request.CorrelationId); + var issueRequest = new IssueTokenApiRequest( + request.SubjectId, + request.TenantId, + correlationId, + request.Provider, + request.ExternalToken); + + var policyRequest = contractAdapter.ToPolicyRequest(issueRequest, "identity.token.issue"); + var policyResponse = await serviceClient.EvaluatePolicyAsync(policyRequest); + if (!permissionGuard.CanAccess(policyResponse)) + { + return ErrorResult(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId); + } + + var serviceRequest = contractAdapter.ToIssueTokenRequest(issueRequest); + var sessionTokens = await serviceClient.StartSessionAsync(serviceRequest, correlationId); + + if (string.IsNullOrWhiteSpace(sessionTokens.AccessToken) || string.IsNullOrWhiteSpace(sessionTokens.RefreshToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_login_failed", "Unable to issue session.", correlationId); + } + + WriteSessionCookies(context, sessionTokens, builder.Configuration); + + var response = new SessionLoginApiResponse( + sessionTokens.SubjectId, + sessionTokens.TenantId, + sessionTokens.Provider, + sessionTokens.ExpiresInSeconds); + + return Results.Ok(response); +}); + +// Compatibility alias kept for existing token-based callers. app.MapPost($"{EndpointConventions.ApiPrefix}/token", async ( IssueTokenApiRequest request, HttpContext context, @@ -54,18 +144,105 @@ app.MapPost($"{EndpointConventions.ApiPrefix}/token", async ( } catch (UnauthorizedAccessException) { - return Results.Unauthorized(); + var correlationId = ResolveCorrelationId(context, normalizedRequest.CorrelationId); + return ErrorResult(StatusCodes.Status401Unauthorized, "unauthorized", "Unauthorized request.", correlationId); } }); app.MapPost($"{EndpointConventions.ApiPrefix}/session/refresh", async ( - RefreshSessionApiRequest request, + RefreshSessionApiRequest? request, HttpContext context, - IRefreshSessionHandler handler) => + IThalosServiceClient serviceClient) => { - var normalizedRequest = request with { CorrelationId = ResolveCorrelationId(context, request.CorrelationId) }; - var response = await handler.HandleAsync(normalizedRequest); - return Results.Ok(response); + var correlationId = ResolveCorrelationId(context, request?.CorrelationId); + var refreshToken = request?.RefreshToken; + if (string.IsNullOrWhiteSpace(refreshToken)) + { + context.Request.Cookies.TryGetValue(SessionRefreshCookieName, out refreshToken); + } + + if (string.IsNullOrWhiteSpace(refreshToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_missing", "Session refresh token is required.", correlationId); + } + + var provider = request?.Provider ?? IdentityAuthProvider.InternalJwt; + var refreshResponse = await serviceClient.RefreshSessionTokensAsync( + new RefreshIdentitySessionRequest(refreshToken, correlationId, provider)); + + if (string.IsNullOrWhiteSpace(refreshResponse.AccessToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_refresh_failed", "Session refresh failed.", correlationId); + } + + WriteSessionCookies(context, refreshResponse, builder.Configuration); + return Results.Ok(new SessionLoginApiResponse( + refreshResponse.SubjectId, + refreshResponse.TenantId, + refreshResponse.Provider, + refreshResponse.ExpiresInSeconds)); +}); + +// Compatibility alias kept for token-first refresh callers. +app.MapPost($"{EndpointConventions.ApiPrefix}/token/refresh", async ( + RefreshSessionApiRequest? request, + HttpContext context, + IThalosServiceClient serviceClient) => +{ + var correlationId = ResolveCorrelationId(context, request?.CorrelationId); + var refreshToken = request?.RefreshToken; + if (string.IsNullOrWhiteSpace(refreshToken)) + { + context.Request.Cookies.TryGetValue(SessionRefreshCookieName, out refreshToken); + } + + if (string.IsNullOrWhiteSpace(refreshToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_missing", "Session refresh token is required.", correlationId); + } + + var provider = request?.Provider ?? IdentityAuthProvider.InternalJwt; + var refreshResponse = await serviceClient.RefreshSessionTokensAsync( + new RefreshIdentitySessionRequest(refreshToken, correlationId, provider)); + + if (string.IsNullOrWhiteSpace(refreshResponse.AccessToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_refresh_failed", "Session refresh failed.", correlationId); + } + + WriteSessionCookies(context, refreshResponse, builder.Configuration); + return Results.Ok(new RefreshSessionApiResponse(refreshResponse.AccessToken, refreshResponse.ExpiresInSeconds)); +}); + +app.MapPost($"{EndpointConventions.ApiPrefix}/session/logout", (HttpContext context) => +{ + DeleteSessionCookies(context, builder.Configuration); + return Results.NoContent(); +}); + +// Compatibility alias for logout callers. +app.MapPost($"{EndpointConventions.ApiPrefix}/logout", (HttpContext context) => +{ + DeleteSessionCookies(context, builder.Configuration); + return Results.NoContent(); +}); + +app.MapGet($"{EndpointConventions.ApiPrefix}/session/me", (HttpContext context) => +{ + var correlationId = ResolveCorrelationId(context); + + if (!context.Request.Cookies.TryGetValue(SessionAccessCookieName, out var accessToken) || + string.IsNullOrWhiteSpace(accessToken)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_missing", "No active session.", correlationId); + } + + if (!TryParseSessionProfile(accessToken, out var meResponse)) + { + return ErrorResult(StatusCodes.Status401Unauthorized, "session_invalid", "Invalid session token.", correlationId); + } + + return Results.Ok(meResponse); }); app.MapHealthChecks("/healthz"); @@ -73,6 +250,11 @@ app.MapHealthChecks("/health"); app.Run(); +IResult ErrorResult(int statusCode, string code, string message, string correlationId) +{ + return Results.Json(new ApiErrorResponse(code, message, correlationId), statusCode: statusCode); +} + string ResolveCorrelationId(HttpContext context, string? preferred = null) { if (!string.IsNullOrWhiteSpace(preferred)) @@ -98,3 +280,79 @@ string ResolveCorrelationId(HttpContext context, string? preferred = null) return context.TraceIdentifier; } + +void WriteSessionCookies(HttpContext context, Thalos.Bff.Application.Sessions.SessionTokensResult tokens, IConfiguration configuration) +{ + var secureCookie = configuration.GetValue("ThalosBff:SessionCookieSecure", false); + var cookieOptions = CreateCookieOptions(secureCookie, tokens.ExpiresInSeconds); + + context.Response.Cookies.Append(SessionAccessCookieName, tokens.AccessToken, cookieOptions); + + var refreshCookieSeconds = Math.Max(tokens.ExpiresInSeconds, 8 * 60 * 60); + context.Response.Cookies.Append( + SessionRefreshCookieName, + tokens.RefreshToken, + CreateCookieOptions(secureCookie, refreshCookieSeconds)); +} + +void DeleteSessionCookies(HttpContext context, IConfiguration configuration) +{ + var secureCookie = configuration.GetValue("ThalosBff:SessionCookieSecure", false); + var options = CreateCookieOptions(secureCookie, 0); + context.Response.Cookies.Delete(SessionAccessCookieName, options); + context.Response.Cookies.Delete(SessionRefreshCookieName, options); +} + +static CookieOptions CreateCookieOptions(bool secure, int expiresInSeconds) +{ + return new CookieOptions + { + HttpOnly = true, + Secure = secure, + SameSite = SameSiteMode.Lax, + Path = "/", + MaxAge = TimeSpan.FromSeconds(Math.Max(0, expiresInSeconds)) + }; +} + +static bool TryParseSessionProfile(string accessToken, out SessionMeApiResponse response) +{ + response = new SessionMeApiResponse(false, string.Empty, string.Empty, IdentityAuthProvider.InternalJwt); + + if (string.IsNullOrWhiteSpace(accessToken)) + { + return false; + } + + var parts = accessToken.Split(':', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + if (parts.Length < 3) + { + return false; + } + + IdentityAuthProvider provider; + string subjectId; + string tenantId; + + if (parts[0].Equals("azure", StringComparison.OrdinalIgnoreCase) && parts.Length >= 4) + { + provider = IdentityAuthProvider.AzureAd; + subjectId = parts[1]; + tenantId = parts[2]; + } + else if (parts[0].Equals("google", StringComparison.OrdinalIgnoreCase) && parts.Length >= 4) + { + provider = IdentityAuthProvider.Google; + subjectId = parts[1]; + tenantId = parts[2]; + } + else + { + provider = IdentityAuthProvider.InternalJwt; + subjectId = parts[0]; + tenantId = parts[1]; + } + + response = new SessionMeApiResponse(true, subjectId, tenantId, provider); + return true; +} diff --git a/src/Thalos.Bff.Rest/Protos/identity_runtime.proto b/src/Thalos.Bff.Rest/Protos/identity_runtime.proto index 02b620b..ca37a19 100644 --- a/src/Thalos.Bff.Rest/Protos/identity_runtime.proto +++ b/src/Thalos.Bff.Rest/Protos/identity_runtime.proto @@ -5,10 +5,44 @@ option csharp_namespace = "Thalos.Service.Grpc"; package thalos.service.grpc; service IdentityRuntime { + rpc StartIdentitySession (StartIdentitySessionGrpcRequest) returns (StartIdentitySessionGrpcResponse); + rpc RefreshIdentitySession (RefreshIdentitySessionGrpcRequest) returns (RefreshIdentitySessionGrpcResponse); rpc IssueIdentityToken (IssueIdentityTokenGrpcRequest) returns (IssueIdentityTokenGrpcResponse); 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 { string subject_id = 1; string tenant_id = 2; diff --git a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs index 99f4d1c..0003480 100644 --- a/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/ContractShapeTests.cs @@ -18,6 +18,17 @@ public class ContractShapeTests Assert.Equal(IdentityAuthProvider.InternalJwt, request.Provider); } + [Fact] + public void SessionLoginApiRequest_WhenCreated_UsesProviderDefault() + { + var request = new SessionLoginApiRequest("user-2", "tenant-2", "corr-456"); + + Assert.Equal("user-2", request.SubjectId); + Assert.Equal("tenant-2", request.TenantId); + Assert.Equal("corr-456", request.CorrelationId); + Assert.Equal(IdentityAuthProvider.InternalJwt, request.Provider); + } + [Fact] public void ThalosBffPackageContract_WhenCreated_UsesBlueprintDescriptorContract() { diff --git a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs index c14f71f..bc82da3 100644 --- a/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/IssueTokenHandlerTests.cs @@ -1,5 +1,6 @@ using Thalos.Bff.Application.Adapters; using Thalos.Bff.Application.Handlers; +using Thalos.Bff.Application.Sessions; using Thalos.Bff.Application.Security; using Thalos.Bff.Contracts.Api; using BuildingBlock.Identity.Contracts.Requests; @@ -25,6 +26,17 @@ public class IssueTokenHandlerTests private sealed class FakeThalosServiceClient : IThalosServiceClient { + public Task StartSessionAsync(IssueIdentityTokenRequest request, string correlationId) + { + return Task.FromResult(new SessionTokensResult( + "token-xyz", + "refresh-xyz", + 1800, + request.SubjectId, + request.TenantId, + request.Provider)); + } + public Task IssueTokenAsync(IssueIdentityTokenRequest request) { return Task.FromResult(new IssueIdentityTokenResponse("token-xyz", 1800)); @@ -39,6 +51,17 @@ public class IssueTokenHandlerTests { return Task.FromResult(new RefreshIdentitySessionResponse("token-refreshed", 1800)); } + + public Task RefreshSessionTokensAsync(RefreshIdentitySessionRequest request) + { + return Task.FromResult(new SessionTokensResult( + "token-refreshed", + request.RefreshToken, + 1800, + "user-1", + "tenant-1", + request.Provider)); + } } private sealed class FakeIdentityEdgeContractAdapter : IIdentityEdgeContractAdapter diff --git a/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs b/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs index b805764..cb11066 100644 --- a/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs +++ b/tests/Thalos.Bff.Application.UnitTests/RefreshSessionHandlerTests.cs @@ -1,5 +1,6 @@ using Thalos.Bff.Application.Adapters; using Thalos.Bff.Application.Handlers; +using Thalos.Bff.Application.Sessions; using Thalos.Bff.Contracts.Api; using BuildingBlock.Identity.Contracts.Requests; using BuildingBlock.Identity.Contracts.Responses; @@ -21,6 +22,17 @@ public class RefreshSessionHandlerTests private sealed class FakeThalosServiceClient : IThalosServiceClient { + public Task StartSessionAsync(IssueIdentityTokenRequest request, string correlationId) + { + return Task.FromResult(new SessionTokensResult( + "token-xyz", + "refresh-xyz", + 1800, + request.SubjectId, + request.TenantId, + request.Provider)); + } + public Task IssueTokenAsync(IssueIdentityTokenRequest request) { return Task.FromResult(new IssueIdentityTokenResponse("token-xyz", 1800)); @@ -35,6 +47,17 @@ public class RefreshSessionHandlerTests { return Task.FromResult(new RefreshIdentitySessionResponse("token-refreshed", 1800)); } + + public Task RefreshSessionTokensAsync(RefreshIdentitySessionRequest request) + { + return Task.FromResult(new SessionTokensResult( + "token-refreshed", + request.RefreshToken, + 1800, + "user-1", + "tenant-1", + request.Provider)); + } } private sealed class FakeIdentityEdgeContractAdapter : IIdentityEdgeContractAdapter