Skip to content

Commit faa0e09

Browse files
Fix multiple issues
- Document the authentication toggle and split-cookie variables. - Malformed logout allowlist entries are now skipped individually while valid origins remain usable. - Added regression coverage for mixed valid and invalid origins. - Vite now uses one fallback-safe `apiBaseURL` for HTTPS detection and both CSP policies. - Production and development still require an explicitly configured API URL; only test mode uses the fallback.
1 parent 273ed6b commit faa0e09

4 files changed

Lines changed: 45 additions & 9 deletions

File tree

portal/frontend/README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,15 @@ pnpm install
4949

5050
Create a local `.env` file from `.env.example` before running or building the portal.
5151

52-
| Variable | Description | Example |
53-
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
54-
| `VITE_API_BASE_URL` | Required base URL for the OpenFGC Portal backend API. Vite embeds this value at build time. | `http://localhost:8080` |
55-
| `VITE_AUTH_LOGOUT_ALLOWED_ORIGINS` | Exact comma-separated origins accepted for logout navigation. Include the IdP origin when using its end-session endpoint. | `https://idp.example.com` |
52+
| Variable | Description | Example |
53+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------- |
54+
| `VITE_API_BASE_URL` | Required base URL for the OpenFGC Portal backend API. Vite embeds this value at build time. | `http://localhost:8080` |
55+
| `VITE_AUTH_ENABLED` | Enables frontend authentication gating; set to `true` for protected deployments. | `true` |
56+
| `VITE_AUTH_ACCESS_TOKEN_PART1_COOKIE` | Cookie name for the readable access-token part. | `portal-at-p1` |
57+
| `VITE_AUTH_REFRESH_TOKEN_PART1_COOKIE` | Cookie name for the readable refresh-token part. | `portal-rt-p1` |
58+
| `VITE_AUTH_ID_TOKEN_PART1_COOKIE` | Cookie name for ID-token part 1. | `portal-id-p1` |
59+
| `VITE_AUTH_ID_TOKEN_PART2_COOKIE` | Cookie name for ID-token part 2. | `portal-id-p2` |
60+
| `VITE_AUTH_LOGOUT_ALLOWED_ORIGINS` | Exact comma-separated origins accepted for logout navigation. Include the IdP origin when using its end-session endpoint. | `https://idp.example.com` |
5661

5762
## Production security headers
5863

portal/frontend/src/__tests__/AuthClient.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,29 @@ describe('portal auth client', () => {
169169
expect(assign).toHaveBeenCalledWith('https://idp.example/logout')
170170
})
171171

172+
it('ignores malformed logout origins without discarding valid configured origins', async () => {
173+
vi.stubEnv('VITE_API_BASE_URL', 'http://api.example')
174+
vi.stubEnv(
175+
'VITE_AUTH_LOGOUT_ALLOWED_ORIGINS',
176+
'not-a-url,ftp://unsupported.example,https://idp.example',
177+
)
178+
setCookie('portal-at-p1', 'access-part')
179+
const fetchMock = vi.fn().mockResolvedValue(
180+
new Response(JSON.stringify({ logoutUrl: 'https://idp.example/logout' }), {
181+
status: 200,
182+
headers: { 'Content-Type': 'application/json' },
183+
}),
184+
)
185+
vi.stubGlobal('fetch', fetchMock)
186+
const assign = vi.fn()
187+
vi.stubGlobal('window', { location: { assign, origin: 'http://portal.example' } })
188+
189+
await logout()
190+
191+
expect(fetchMock).toHaveBeenCalledOnce()
192+
expect(assign).toHaveBeenCalledWith('https://idp.example/logout')
193+
})
194+
172195
it('rejects a BFF-returned logout URL outside the navigation allowlist', async () => {
173196
vi.stubEnv('VITE_API_BASE_URL', 'http://api.example')
174197
vi.stubEnv('VITE_AUTH_LOGOUT_ALLOWED_ORIGINS', 'https://idp.example')

portal/frontend/src/utils/authClient.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,18 @@ function allowedNavigationOrigins(): Set<string> {
3939
}
4040

4141
const configured = import.meta.env.VITE_AUTH_LOGOUT_ALLOWED_ORIGINS as string | undefined
42-
configured
42+
const configuredOrigins = configured
4343
?.split(',')
4444
.map((origin) => origin.trim())
4545
.filter(Boolean)
46-
.forEach((origin) => origins.add(httpURL(origin).origin))
46+
47+
configuredOrigins?.forEach((origin) => {
48+
try {
49+
origins.add(httpURL(origin).origin)
50+
} catch {
51+
// Invalid configured entries fail closed without disabling valid origins.
52+
}
53+
})
4754

4855
return origins
4956
}

portal/frontend/vite.config.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,21 @@ function securityHeadersPlugin(policy: string, metaPolicy: string): Plugin {
4646
// https://vite.dev/config/
4747
export default defineConfig(({ mode }) => {
4848
const env = loadEnv(mode, process.cwd(), 'VITE_')
49+
const apiBaseURL = env.VITE_API_BASE_URL || 'http://localhost:8080'
4950

5051
if (mode !== 'test' && !env.VITE_API_BASE_URL) {
5152
throw new Error('VITE_API_BASE_URL is required. Create a .env file from .env.example.')
5253
}
5354

5455
const production = mode === 'production'
55-
const upgradeInsecureRequests = production && env.VITE_API_BASE_URL.startsWith('https://')
56+
const upgradeInsecureRequests = production && apiBaseURL.startsWith('https://')
5657
const policy = contentSecurityPolicy({
57-
apiBaseURL: env.VITE_API_BASE_URL || 'http://localhost:8080',
58+
apiBaseURL,
5859
upgradeInsecureRequests,
5960
})
6061
// frame-ancestors is supported only in the HTTP header, not a CSP meta element.
6162
const metaPolicy = contentSecurityPolicy({
62-
apiBaseURL: env.VITE_API_BASE_URL || 'http://localhost:8080',
63+
apiBaseURL,
6364
includeFrameAncestors: false,
6465
upgradeInsecureRequests,
6566
})

0 commit comments

Comments
 (0)