Skip to content

Commit 780a54a

Browse files
authored
Fix critical antiforgery issue and ensure compatibility of self-contained systems (#713)
### Summary & motivation Resolve several issues introduced by the recent addition of antiforgery protection and improve cross-system compatibility in PlatformPlatform. Fixes and improvements include: 1. The internal `/internal-api/account-management/authentication/refresh-authentication-tokens` endpoint now explicitly disables antiforgery checks, as it did not receive the required antiforgery cookie or header, causing users to be logged out when trying to refresh the access token. 2. Antiforgery logic has been extracted into a shared module in the web app infrastructure. This provides: - A reusable fetch wrapper that automatically adds the antiforgery token for non-GET requests. - A reusable middleware for TanStack Query (`openapi-fetch`) clients. - Simpler integration in LocaleSwitcher and other components making direct fetch calls. 3. A bug was fixed where antiforgery validation failed across self-contained systems due to different Data Protection keys being used. To resolve this: - A common ApplicationName is now set when running locally, ensuring that all systems share encryption keys for signing antiforgery tokens. - In Azure, Azure Container App is configured to automatically and securely share data protection keys. Together, these changes ensure that antiforgery protection works reliably across all parts of the platform, both locally and in production. ### Downstream projects Update your API client initialization and application bootstrap logic: 1. Copy the `back-office/WebApp/bootstrap.tsx` into your self-contained system. A new `initializeHttpInterceptor()` call has been introduced including some formatting. 2. Copy `back-office/WebApp/client.ts` into your-self-contained-system A new `apiClient.use(createAntiforgeryMiddleware());` has been added. 3. If you are using fetch directly in your code, use the `fetchWithAntiforgeryToken` helper: ``` import { fetchWithAntiforgeryToken } from "@repo/infrastructure/http/antiforgeryTokenHandler"; await fetchWithAntiforgeryToken("/your-endpoint", { method: "POST", body: ... }); ``` See example in the `shared-webapp/infrastructure/translations/LocaleSwitcher.tsx` 4. If you have any `internal/**` endpoints called from other self-contained systems, you may need to add `.DisableAntiforgery();` ### Checklist - [x] I have added tests, or done manual regression tests - [x] I have updated the documentation, if necessary
2 parents 0b2ea84 + 9307506 commit 780a54a

File tree

8 files changed

+107
-30
lines changed

8 files changed

+107
-30
lines changed

application/account-management/Api/Endpoints/AuthenticationEndpoints.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,6 @@ public void MapEndpoints(IEndpointRouteBuilder routes)
3434
// Note: This endpoint must be called with the refresh token as Bearer token in the Authorization header
3535
routes.MapPost("/internal-api/account-management/authentication/refresh-authentication-tokens", async Task<ApiResult> (IMediator mediator)
3636
=> await mediator.Send(new RefreshAuthenticationTokensCommand())
37-
);
37+
).DisableAntiforgery();
3838
}
3939
}

application/account-management/WebApp/bootstrap.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import "@repo/ui/tailwind.css";
22
import { router } from "@/shared/lib/router/router";
33
import { ApplicationInsightsProvider } from "@repo/infrastructure/applicationInsights/ApplicationInsightsProvider";
4+
import { initializeHttpInterceptors } from "@repo/infrastructure/http/antiforgeryTokenHandler";
45
import { Translation } from "@repo/infrastructure/translations/Translation";
56
import { RouterProvider } from "@tanstack/react-router";
67
import React from "react";
7-
// biome-ignore lint/style/useNamingConvention: ReactDOM is a standard library name with consecutive uppercase letters
8-
import ReactDOM from "react-dom/client";
8+
import reactDom from "react-dom/client";
9+
10+
// Initialize HTTP interceptors to automatically handle antiforgery tokens
11+
initializeHttpInterceptors();
912

1013
const { TranslationProvider } = await Translation.create(
1114
(locale) => import(`@/shared/translations/locale/${locale}.ts`)
@@ -17,7 +20,7 @@ if (!rootElement) {
1720
throw new Error("Root element not found");
1821
}
1922

20-
ReactDOM.createRoot(rootElement).render(
23+
reactDom.createRoot(rootElement).render(
2124
<React.StrictMode>
2225
<TranslationProvider>
2326
<ApplicationInsightsProvider>

application/account-management/WebApp/shared/lib/api/client.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,17 @@
11
import { createAuthenticationMiddleware } from "@repo/infrastructure/auth/AuthenticationMiddleware";
2-
import type { components, paths } from "./api.generated";
3-
2+
import { createAntiforgeryMiddleware } from "@repo/infrastructure/http/antiforgeryTokenHandler";
43
import createFetchClient from "openapi-fetch";
54
import createClient from "openapi-react-query";
5+
import type { components, paths } from "./api.generated";
66

77
export * from "./api.generated.d";
88

9-
const apiClient = createFetchClient<paths>({
9+
export const apiClient = createFetchClient<paths>({
1010
baseUrl: import.meta.env.PUBLIC_URL
1111
});
12-
apiClient.use(createAuthenticationMiddleware());
1312

14-
// Add middleware to include antiforgery token only for non-GET requests
15-
apiClient.use({
16-
onRequest: (params) => {
17-
const request = params.request;
18-
if (request instanceof Request && request.method !== "GET") {
19-
request.headers.set("x-xsrf-token", getAntiforgeryToken());
20-
}
21-
return request;
22-
}
23-
});
24-
25-
// Get the antiforgery token from the meta tag
26-
const getAntiforgeryToken = () => {
27-
const metaTag = document.querySelector('meta[name="antiforgeryToken"]');
28-
return metaTag?.getAttribute("content") ?? "";
29-
};
13+
apiClient.use(createAuthenticationMiddleware());
14+
apiClient.use(createAntiforgeryMiddleware());
3015

3116
export const api = createClient(apiClient);
3217

application/back-office/WebApp/bootstrap.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import "@repo/ui/tailwind.css";
22
import { router } from "@/shared/lib/router/router";
33
import { ApplicationInsightsProvider } from "@repo/infrastructure/applicationInsights/ApplicationInsightsProvider";
4+
import { initializeHttpInterceptors } from "@repo/infrastructure/http/antiforgeryTokenHandler";
45
import { Translation } from "@repo/infrastructure/translations/Translation";
56
import { RouterProvider } from "@tanstack/react-router";
67
import React from "react";
7-
// biome-ignore lint/style/useNamingConvention: ReactDOM is a standard library name with consecutive uppercase letters
8-
import ReactDOM from "react-dom/client";
8+
import reactDom from "react-dom/client";
9+
10+
// Initialize HTTP interceptors to automatically handle antiforgery tokens
11+
initializeHttpInterceptors();
912

1013
const { TranslationProvider } = await Translation.create(
1114
(locale) => import(`@/shared/translations/locale/${locale}.ts`)
@@ -17,7 +20,7 @@ if (!rootElement) {
1720
throw new Error("Root element not found");
1821
}
1922

20-
ReactDOM.createRoot(rootElement).render(
23+
reactDom.createRoot(rootElement).render(
2124
<React.StrictMode>
2225
<TranslationProvider>
2326
<ApplicationInsightsProvider>

application/back-office/WebApp/shared/lib/api/client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { createAuthenticationMiddleware } from "@repo/infrastructure/auth/AuthenticationMiddleware";
2-
import type { components, paths } from "./api.generated";
3-
2+
import { createAntiforgeryMiddleware } from "@repo/infrastructure/http/antiforgeryTokenHandler";
43
import createFetchClient from "openapi-fetch";
54
import createClient from "openapi-react-query";
5+
import type { components, paths } from "./api.generated";
66

77
export * from "./api.generated.d";
88

99
export const apiClient = createFetchClient<paths>({
1010
baseUrl: import.meta.env.PUBLIC_URL
1111
});
12+
1213
apiClient.use(createAuthenticationMiddleware());
14+
apiClient.use(createAntiforgeryMiddleware());
1315

1416
export const api = createClient(apiClient);
1517

application/shared-kernel/SharedKernel/Configuration/ApiDependencyConfiguration.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.AspNetCore.Authentication.JwtBearer;
22
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.DataProtection;
34
using Microsoft.AspNetCore.Hosting;
45
using Microsoft.AspNetCore.HttpOverrides;
56
using Microsoft.Extensions.DependencyInjection;
@@ -62,6 +63,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services
6263
.AddApiEndpoints(assemblies)
6364
.AddOpenApiConfiguration(assemblies)
6465
.AddAuthConfiguration()
66+
.AddCrossServiceDataProtection()
6567
.AddAntiforgery(options =>
6668
{
6769
options.Cookie.Name = AuthenticationTokenHttpKeys.AntiforgeryTokenCookieName;
@@ -167,6 +169,20 @@ private static IServiceCollection AddAuthConfiguration(this IServiceCollection s
167169
return services.AddAuthorization();
168170
}
169171

172+
private static IServiceCollection AddCrossServiceDataProtection(this IServiceCollection services)
173+
{
174+
// Configure shared data protection to ensure encrypted data can be shared across all self-contained systems
175+
var dataProtection = services.AddDataProtection();
176+
177+
if (!SharedInfrastructureConfiguration.IsRunningInAzure)
178+
{
179+
// Set a common application name for all self-contained systems for local development (handled automatically by Azure Container Apps Environment)
180+
dataProtection.SetApplicationName("PlatformPlatform");
181+
}
182+
183+
return services;
184+
}
185+
170186
public static IServiceCollection AddHttpForwardHeaders(this IServiceCollection services)
171187
{
172188
// Ensure correct client IP addresses are set for requests
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* HTTP utilities for handling antiforgery tokens in fetch requests
3+
*
4+
* This module provides utilities to automatically add antiforgery tokens to HTTP requests
5+
* for both native fetch and openapi-fetch clients
6+
*/
7+
8+
/**
9+
* Gets the antiforgery token from the meta tag
10+
*/
11+
export const getAntiforgeryToken = (): string => {
12+
const metaTag = document.querySelector('meta[name="antiforgeryToken"]');
13+
return metaTag?.getAttribute("content") ?? "";
14+
};
15+
16+
// Store the original fetch function to avoid recursion
17+
const originalFetch = window.fetch;
18+
19+
/**
20+
* Fetch function that automatically adds the antiforgery token to non-GET requests
21+
*/
22+
export const fetchWithAntiforgeryToken = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
23+
const method = init?.method?.toUpperCase() ?? "GET";
24+
25+
// Create a new init object with the original properties
26+
const enhancedInit: RequestInit = { ...init };
27+
28+
// Add antiforgery token for non-GET requests
29+
if (method !== "GET") {
30+
enhancedInit.headers = {
31+
...enhancedInit.headers,
32+
"x-xsrf-token": getAntiforgeryToken()
33+
};
34+
}
35+
36+
// Call the original fetch with the enhanced init to avoid recursion
37+
return originalFetch.call(window, input, enhancedInit);
38+
};
39+
40+
type OpenApiFetchRequestParams = {
41+
request: Request;
42+
};
43+
44+
/**
45+
* Initialize HTTP interceptors to add antiforgery tokens to all non-GET requests
46+
*
47+
* Call this function once during application startup to ensure all HTTP calls
48+
* have the necessary antiforgery tokens
49+
*/
50+
export const initializeHttpInterceptors = (): void => {
51+
window.fetch = (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
52+
return fetchWithAntiforgeryToken(input, init);
53+
};
54+
};
55+
56+
/**
57+
* Creates middleware for openapi-fetch clients to add antiforgery tokens
58+
*/
59+
export const createAntiforgeryMiddleware = () => ({
60+
onRequest: ({ request }: OpenApiFetchRequestParams) => {
61+
// Only add the token for non-GET requests
62+
if (request.method !== "GET") {
63+
request.headers.set("x-xsrf-token", getAntiforgeryToken());
64+
}
65+
return request;
66+
}
67+
});

application/shared-webapp/infrastructure/translations/LocaleSwitcher.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useLingui } from "@lingui/react";
22
import type { Key } from "@react-types/shared";
33
import { AuthenticationContext } from "@repo/infrastructure/auth/AuthenticationProvider";
4+
import { fetchWithAntiforgeryToken } from "@repo/infrastructure/http/antiforgeryTokenHandler";
45
import { Button } from "@repo/ui/components/Button";
56
import { Menu, MenuItem, MenuTrigger } from "@repo/ui/components/Menu";
67
import { CheckIcon, LanguagesIcon } from "lucide-react";
@@ -26,7 +27,7 @@ export function LocaleSwitcher({ "aria-label": ariaLabel }: { "aria-label": stri
2627
const locale = key.toString() as Locale;
2728
if (locale !== currentLocale) {
2829
if (userInfo?.isAuthenticated) {
29-
fetch("/api/account-management/users/me/change-locale", {
30+
fetchWithAntiforgeryToken("/api/account-management/users/me/change-locale", {
3031
method: "PUT",
3132
headers: { "Content-Type": "application/json" },
3233
body: JSON.stringify({ locale })

0 commit comments

Comments
 (0)