From ff622141067b09fb34caf2858cda9a907c05c7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Ren=C3=A9=20White=20Enciso?= Date: Wed, 11 Mar 2026 10:37:53 -0600 Subject: [PATCH] feat(pos-transactions-bff): harden session auth enforcement --- docs/runbooks/containerization.md | 2 + docs/security/auth-enforcement.md | 5 ++ src/Pos.Transactions.Bff.Rest/Program.cs | 50 +++++++++++++++---- .../Security/SessionMePayloadParser.cs | 29 +++++++++++ 4 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 src/Pos.Transactions.Bff.Rest/Security/SessionMePayloadParser.cs diff --git a/docs/runbooks/containerization.md b/docs/runbooks/containerization.md index 276ffc1..fae9bd6 100644 --- a/docs/runbooks/containerization.md +++ b/docs/runbooks/containerization.md @@ -23,6 +23,8 @@ docker run --rm -p 8080:8080 --name pos-transactions-bff agilewebs/pos-transacti ## Runtime Notes - Exposes REST edge endpoints for transaction summary and payment capture. +- Requires `ThalosAuth__BaseAddress` to resolve Thalos session introspection endpoint. +- Returns standardized auth failures (`401|403|503`) with `x-correlation-id` propagation. ## Health Endpoint Consistency diff --git a/docs/security/auth-enforcement.md b/docs/security/auth-enforcement.md index 02a9b70..7fb8a6f 100644 --- a/docs/security/auth-enforcement.md +++ b/docs/security/auth-enforcement.md @@ -38,8 +38,13 @@ Standard auth error payload: - `401`: missing or invalid session - `403`: permission denied by identity service +- `503`: identity service unavailable or timeout (`identity_unavailable|identity_timeout`) ## Correlation - Incoming/outgoing correlation header: `x-correlation-id` - Correlation ID is forwarded to Thalos session validation call. + +## Validation Rule + +- Successful session introspection must also include `isAuthenticated=true` in Thalos response payload. diff --git a/src/Pos.Transactions.Bff.Rest/Program.cs b/src/Pos.Transactions.Bff.Rest/Program.cs index 97c1149..254eaa0 100644 --- a/src/Pos.Transactions.Bff.Rest/Program.cs +++ b/src/Pos.Transactions.Bff.Rest/Program.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.Primitives; using Pos.Transactions.Bff.Application.Adapters; using Pos.Transactions.Bff.Application.Handlers; using Pos.Transactions.Bff.Contracts.Requests; +using Pos.Transactions.Bff.Rest.Security; const string CorrelationHeaderName = "x-correlation-id"; const string SessionAccessCookieName = "thalos_session"; @@ -109,21 +110,50 @@ async Task EnforceSessionAsync( request.Headers.TryAddWithoutValidation("Cookie", cookieHeader); } - using var response = await httpClientFactory.CreateClient("ThalosAuth").SendAsync(request, ct); - - if (response.StatusCode == HttpStatusCode.Forbidden) + HttpResponseMessage response; + try { - return ErrorResponse(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId); + response = await httpClientFactory.CreateClient("ThalosAuth").SendAsync(request, ct); + } + catch (HttpRequestException) + { + return ErrorResponse( + StatusCodes.Status503ServiceUnavailable, + "identity_unavailable", + "Identity service is temporarily unavailable.", + correlationId); + } + catch (TaskCanceledException) + { + return ErrorResponse( + StatusCodes.Status503ServiceUnavailable, + "identity_timeout", + "Identity service did not respond in time.", + correlationId); } - if (response.StatusCode == HttpStatusCode.Unauthorized) + using (response) { - return ErrorResponse(StatusCodes.Status401Unauthorized, "unauthorized", "Unauthorized request.", correlationId); - } + if (response.StatusCode == HttpStatusCode.Forbidden) + { + return ErrorResponse(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId); + } - if (!response.IsSuccessStatusCode) - { - return ErrorResponse(StatusCodes.Status401Unauthorized, "session_invalid", "Session validation failed.", correlationId); + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + return ErrorResponse(StatusCodes.Status401Unauthorized, "unauthorized", "Unauthorized request.", correlationId); + } + + if (!response.IsSuccessStatusCode) + { + return ErrorResponse(StatusCodes.Status401Unauthorized, "session_invalid", "Session validation failed.", correlationId); + } + + var payload = await response.Content.ReadAsStringAsync(ct); + if (!SessionMePayloadParser.IsAuthenticated(payload)) + { + return ErrorResponse(StatusCodes.Status401Unauthorized, "session_invalid", "Session validation failed.", correlationId); + } } return null; diff --git a/src/Pos.Transactions.Bff.Rest/Security/SessionMePayloadParser.cs b/src/Pos.Transactions.Bff.Rest/Security/SessionMePayloadParser.cs new file mode 100644 index 0000000..ed8b6ea --- /dev/null +++ b/src/Pos.Transactions.Bff.Rest/Security/SessionMePayloadParser.cs @@ -0,0 +1,29 @@ +using System.Text.Json; + +namespace Pos.Transactions.Bff.Rest.Security; + +public static class SessionMePayloadParser +{ + public static bool IsAuthenticated(string payload) + { + if (string.IsNullOrWhiteSpace(payload)) + { + return false; + } + + try + { + using var document = JsonDocument.Parse(payload); + if (!document.RootElement.TryGetProperty("isAuthenticated", out var value)) + { + return false; + } + + return value.ValueKind == JsonValueKind.True; + } + catch (JsonException) + { + return false; + } + } +}