diff --git a/frontend/src/auth/AuthProvider.test.tsx b/frontend/src/auth/AuthProvider.test.tsx
index e3023b1e28..8ddae4ca70 100644
--- a/frontend/src/auth/AuthProvider.test.tsx
+++ b/frontend/src/auth/AuthProvider.test.tsx
@@ -161,6 +161,7 @@ describe("AuthProvider", () => {
await waitFor(() => {
expect(screen.getByText("Authentication Error")).toBeVisible();
expect(screen.getByText("Config fetch failed")).toBeVisible();
+ expect(screen.getByText("Reload the page to try again.")).toBeVisible();
});
});
diff --git a/frontend/src/auth/AuthProvider.tsx b/frontend/src/auth/AuthProvider.tsx
index 34577c7b9b..f707ffa159 100644
--- a/frontend/src/auth/AuthProvider.tsx
+++ b/frontend/src/auth/AuthProvider.tsx
@@ -119,6 +119,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
Authentication Error
{error}
+
Reload the page to try again.
)
}
diff --git a/frontend/src/auth/msalConfig.test.ts b/frontend/src/auth/msalConfig.test.ts
index bda21bf5fa..c9eb612b3b 100644
--- a/frontend/src/auth/msalConfig.test.ts
+++ b/frontend/src/auth/msalConfig.test.ts
@@ -73,22 +73,28 @@ describe("msalConfig", () => {
expect(global.fetch).toHaveBeenCalledWith("/api/auth/config");
});
- it("returns empty config when response is not ok", async () => {
- (global.fetch as jest.Mock).mockResolvedValue({ ok: false });
+ it("throws when response is not ok (transient failure is not auth-disabled)", async () => {
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: false,
+ status: 503,
+ statusText: "Service Unavailable",
+ });
const { fetchAuthConfig } = await import("./msalConfig");
- const result = await fetchAuthConfig();
- expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" });
+ await expect(fetchAuthConfig()).rejects.toThrow("/api/auth/config returned 503 Service Unavailable");
});
- it("returns empty config on network error", async () => {
- (global.fetch as jest.Mock).mockRejectedValue(new Error("Network error"));
+ it("throws on network error (transient failure is not auth-disabled)", async () => {
+ const networkError = new Error("Network error");
+ (global.fetch as jest.Mock).mockRejectedValue(networkError);
const { fetchAuthConfig } = await import("./msalConfig");
- const result = await fetchAuthConfig();
- expect(result).toEqual({ clientId: "", tenantId: "", allowedGroupIds: "" });
+ await expect(fetchAuthConfig()).rejects.toMatchObject({
+ message: "Failed to reach /api/auth/config: Network error",
+ cause: networkError,
+ });
});
});
});
diff --git a/frontend/src/auth/msalConfig.ts b/frontend/src/auth/msalConfig.ts
index c63e3a68cf..a99aebc3fd 100644
--- a/frontend/src/auth/msalConfig.ts
+++ b/frontend/src/auth/msalConfig.ts
@@ -24,17 +24,26 @@ export interface AuthConfig {
}
export async function fetchAuthConfig(): Promise {
+ let response: Response
try {
- const response = await fetch('/api/auth/config')
- if (!response.ok) {
- // Auth endpoint not available — treat as auth disabled
- return { clientId: '', tenantId: '', allowedGroupIds: '' }
- }
- return (await response.json()) as AuthConfig
- } catch {
- // Network error (e.g., backend not running yet) — treat as auth disabled
- return { clientId: '', tenantId: '', allowedGroupIds: '' }
+ response = await fetch('/api/auth/config')
+ } catch (error) {
+ // A network error (e.g., backend not running yet) is a transient
+ // infrastructure failure, not proof that auth is disabled. Surface it so
+ // AuthProvider can show its error state instead of rendering the shell
+ // while protected APIs return 401.
+ const fetchError = new Error(
+ `Failed to reach /api/auth/config: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ // ErrorOptions requires ES2022, while this project targets ES2020.
+ Object.defineProperty(fetchError, 'cause', { value: error })
+ throw fetchError
}
+ if (!response.ok) {
+ // HTTP-level failures on the config endpoint are equally inconclusive.
+ throw new Error(`/api/auth/config returned ${response.status} ${response.statusText}`)
+ }
+ return (await response.json()) as AuthConfig
}
export function buildMsalConfig(authConfig: AuthConfig): Configuration {