Compare commits
No commits in common. "c2c6c41965b7832e6615a8a87fc462673106a832" and "350d543a67ef87819b701dad3f7775b47fd742ae" have entirely different histories.
c2c6c41965
...
350d543a67
@ -7,8 +7,6 @@
|
|||||||
## Endpoint Baseline
|
## Endpoint Baseline
|
||||||
|
|
||||||
- `GET /api/furniture/{furnitureId}/availability`
|
- `GET /api/furniture/{furnitureId}/availability`
|
||||||
- Auth required: valid Thalos session (`thalos_session` or `thalos_refresh` cookie, validated via `/api/identity/session/me`)
|
|
||||||
- Anonymous exceptions: `/health`, `/healthz`
|
|
||||||
|
|
||||||
## Edge Responsibilities
|
## Edge Responsibilities
|
||||||
|
|
||||||
|
|||||||
@ -23,8 +23,6 @@ docker run --rm -p 8080:8080 --name furniture-bff agilewebs/furniture-bff:dev
|
|||||||
## Runtime Notes
|
## Runtime Notes
|
||||||
|
|
||||||
- Requires `FurnitureService__GrpcAddress` to target furniture-service in distributed runs.
|
- Requires `FurnitureService__GrpcAddress` to target furniture-service in distributed runs.
|
||||||
- Requires `ThalosAuth__BaseAddress` to target thalos-bff session introspection endpoint.
|
|
||||||
- For browser usage, configure `FurnitureBff__AllowedOrigins` with explicit origins (not `*`) so cookie credentials are permitted.
|
|
||||||
- gRPC client contract protobuf is vendored at `src/Furniture.Bff.Rest/Protos/furniture_runtime.proto` to keep image builds repo-local.
|
- gRPC client contract protobuf is vendored at `src/Furniture.Bff.Rest/Protos/furniture_runtime.proto` to keep image builds repo-local.
|
||||||
|
|
||||||
## Health Endpoint Consistency
|
## Health Endpoint Consistency
|
||||||
|
|||||||
@ -38,14 +38,8 @@ Standard auth error payload:
|
|||||||
|
|
||||||
- `401`: missing or invalid session
|
- `401`: missing or invalid session
|
||||||
- `403`: permission denied by identity service
|
- `403`: permission denied by identity service
|
||||||
- `503`: identity service unavailable or timeout during session introspection (`identity_unavailable|identity_timeout`)
|
|
||||||
|
|
||||||
## Correlation
|
## Correlation
|
||||||
|
|
||||||
- Incoming/outgoing correlation header: `x-correlation-id`
|
- Incoming/outgoing correlation header: `x-correlation-id`
|
||||||
- Correlation ID is forwarded to Thalos session validation call.
|
- Correlation ID is forwarded to Thalos session validation call.
|
||||||
|
|
||||||
## CORS and Cookie Propagation
|
|
||||||
|
|
||||||
- When `FurnitureBff:AllowedOrigins` is explicit (non-`*`), the BFF enables credentials so browser session cookies are forwarded.
|
|
||||||
- Wildcard origins remain unsupported for credentialed browser calls by design.
|
|
||||||
|
|||||||
@ -6,7 +6,6 @@ using Furniture.Bff.Application.Handlers;
|
|||||||
using Furniture.Bff.Contracts.Api;
|
using Furniture.Bff.Contracts.Api;
|
||||||
using Furniture.Bff.Rest.Adapters;
|
using Furniture.Bff.Rest.Adapters;
|
||||||
using Furniture.Bff.Rest.Endpoints;
|
using Furniture.Bff.Rest.Endpoints;
|
||||||
using Furniture.Bff.Rest.Security;
|
|
||||||
using Furniture.Service.Grpc;
|
using Furniture.Service.Grpc;
|
||||||
using Microsoft.Extensions.Primitives;
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
@ -41,8 +40,7 @@ builder.Services.AddCors(options =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cookie-based session propagation requires explicit allowed origins and credentials.
|
policy.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader();
|
||||||
policy.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
builder.Services.AddGrpcClient<FurnitureRuntime.FurnitureRuntimeClient>(options =>
|
builder.Services.AddGrpcClient<FurnitureRuntime.FurnitureRuntimeClient>(options =>
|
||||||
@ -134,50 +132,21 @@ async Task<IResult?> EnforceSessionAsync(
|
|||||||
request.Headers.TryAddWithoutValidation("Cookie", cookieHeader);
|
request.Headers.TryAddWithoutValidation("Cookie", cookieHeader);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpResponseMessage response;
|
using var response = await httpClientFactory.CreateClient("ThalosAuth").SendAsync(request, ct);
|
||||||
try
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Forbidden)
|
||||||
{
|
{
|
||||||
response = await httpClientFactory.CreateClient("ThalosAuth").SendAsync(request, ct);
|
return ErrorResponse(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId);
|
||||||
}
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using (response)
|
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||||
{
|
{
|
||||||
if (response.StatusCode == HttpStatusCode.Forbidden)
|
return ErrorResponse(StatusCodes.Status401Unauthorized, "unauthorized", "Unauthorized request.", correlationId);
|
||||||
{
|
}
|
||||||
return ErrorResponse(StatusCodes.Status403Forbidden, "forbidden", "Permission denied.", correlationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
if (!response.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
return ErrorResponse(StatusCodes.Status401Unauthorized, "unauthorized", "Unauthorized request.", correlationId);
|
return ErrorResponse(StatusCodes.Status401Unauthorized, "session_invalid", "Session validation failed.", 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;
|
return null;
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Furniture.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -18,6 +18,5 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Furniture.Bff.Application\Furniture.Bff.Application.csproj" />
|
<ProjectReference Include="..\..\src\Furniture.Bff.Application\Furniture.Bff.Application.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
<ProjectReference Include="..\..\src\Furniture.Bff.Contracts\Furniture.Bff.Contracts.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Furniture.Bff.Rest\Furniture.Bff.Rest.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
using Furniture.Bff.Rest.Security;
|
|
||||||
|
|
||||||
namespace Furniture.Bff.Application.UnitTests;
|
|
||||||
|
|
||||||
public class SessionMePayloadParserTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void IsAuthenticated_WhenPayloadContainsTrueFlag_ReturnsTrue()
|
|
||||||
{
|
|
||||||
const string payload = "{\"isAuthenticated\":true,\"subjectId\":\"demo-user\"}";
|
|
||||||
|
|
||||||
var result = SessionMePayloadParser.IsAuthenticated(payload);
|
|
||||||
|
|
||||||
Assert.True(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IsAuthenticated_WhenPayloadContainsFalseFlag_ReturnsFalse()
|
|
||||||
{
|
|
||||||
const string payload = "{\"isAuthenticated\":false}";
|
|
||||||
|
|
||||||
var result = SessionMePayloadParser.IsAuthenticated(payload);
|
|
||||||
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IsAuthenticated_WhenPayloadIsInvalid_ReturnsFalse()
|
|
||||||
{
|
|
||||||
var result = SessionMePayloadParser.IsAuthenticated("{invalid");
|
|
||||||
|
|
||||||
Assert.False(result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user