merge: integrate furniture-bff auth and web updates
This commit is contained in:
commit
c2c6c41965
@ -7,6 +7,8 @@
|
||||
## Endpoint Baseline
|
||||
|
||||
- `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
|
||||
|
||||
|
||||
@ -23,6 +23,8 @@ docker run --rm -p 8080:8080 --name furniture-bff agilewebs/furniture-bff:dev
|
||||
## Runtime Notes
|
||||
|
||||
- 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.
|
||||
|
||||
## Health Endpoint Consistency
|
||||
|
||||
@ -38,8 +38,14 @@ Standard auth error payload:
|
||||
|
||||
- `401`: missing or invalid session
|
||||
- `403`: permission denied by identity service
|
||||
- `503`: identity service unavailable or timeout during session introspection (`identity_unavailable|identity_timeout`)
|
||||
|
||||
## Correlation
|
||||
|
||||
- Incoming/outgoing correlation header: `x-correlation-id`
|
||||
- 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,6 +6,7 @@ using Furniture.Bff.Application.Handlers;
|
||||
using Furniture.Bff.Contracts.Api;
|
||||
using Furniture.Bff.Rest.Adapters;
|
||||
using Furniture.Bff.Rest.Endpoints;
|
||||
using Furniture.Bff.Rest.Security;
|
||||
using Furniture.Service.Grpc;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
@ -40,7 +41,8 @@ builder.Services.AddCors(options =>
|
||||
return;
|
||||
}
|
||||
|
||||
policy.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader();
|
||||
// Cookie-based session propagation requires explicit allowed origins and credentials.
|
||||
policy.WithOrigins(allowedOrigins).AllowAnyMethod().AllowAnyHeader().AllowCredentials();
|
||||
});
|
||||
});
|
||||
builder.Services.AddGrpcClient<FurnitureRuntime.FurnitureRuntimeClient>(options =>
|
||||
@ -132,21 +134,50 @@ async Task<IResult?> 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;
|
||||
|
||||
29
src/Furniture.Bff.Rest/Security/SessionMePayloadParser.cs
Normal file
29
src/Furniture.Bff.Rest/Security/SessionMePayloadParser.cs
Normal file
@ -0,0 +1,29 @@
|
||||
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,5 +18,6 @@
|
||||
<ItemGroup>
|
||||
<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.Rest\Furniture.Bff.Rest.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
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