Skip to content

Commit 4f5f139

Browse files
committed
fix(ui): address SSO PR review feedback (#2045)
- Use type-only imports in AuthContext (ReactNode, CurrentUser, AuthStatus). - Read NEXT_PUBLIC_SSO_REDIRECT_PATH in the client AuthContext (client components cannot read non-NEXT_PUBLIC env vars) and inject it from the Helm chart alongside SSO_REDIRECT_PATH, so ui.auth.ssoRedirectPath actually affects the re-auth redirect. - Use window.location.assign(...) for the re-auth redirect. - Clarify in docs that oauth2-proxy cookie-refresh + offline_access is opt-in, not a chart default (default scope is "openid profile email groups"). - Correct EP wording on login page (server) vs AuthContext (client) env vars. Signed-off-by: Dmytro Rashko <dmitriy.rashko@amdocs.com>
1 parent a29a551 commit 4f5f139

5 files changed

Lines changed: 29 additions & 14 deletions

File tree

design/EP-2045-sso-session-expiry-redirect.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,10 @@ successful `authenticated` result.
8787

8888
- `ui/src/components/UserMenu.tsx` — reflects the new status (e.g. distinguishes a
8989
logged-out/unsecured menu from an authenticated one).
90-
- `ui/src/app/login/page.tsx` — aligns the branded login page's SSO redirect path
91-
with `SSO_REAUTH_PATH`.
90+
- `ui/src/app/login/page.tsx` — the branded login page (a server component) reads
91+
the server-side `SSO_REDIRECT_PATH`. `AuthContext` (a client component) reads the
92+
client-exposed `NEXT_PUBLIC_SSO_REDIRECT_PATH`; both default to `/oauth2/start`
93+
and the Helm chart injects both from `ui.auth.ssoRedirectPath`.
9294
- `docs/OIDC_PROXY_AUTH_ARCHITECTURE.md` — documents the three states and the
9395
re-auth flow.
9496

docs/OIDC_PROXY_AUTH_ARCHITECTURE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ flowchart TD
137137
style I fill:#ff9,stroke:#333
138138
```
139139

140-
**Design rationale**: oauth2-proxy gates access using its session cookie (valid up to `cookie-expire`, default 168h), while the UI derives the user from the forwarded id_token. These lifetimes are decoupled, so the id_token can go stale while the session cookie is still valid. To keep them aligned, oauth2-proxy is configured with `cookie-refresh` (+ `offline_access` scope) to refresh the id_token. As a safety net, `getCurrentUser()` distinguishes three states:
140+
**Design rationale**: oauth2-proxy gates access using its session cookie (valid up to `cookie-expire`, default 168h), while the UI derives the user from the forwarded id_token. These lifetimes are decoupled, so the id_token can go stale while the session cookie is still valid. To keep them aligned, oauth2-proxy *can* be configured with `cookie-refresh` (and the `offline_access` scope) to refresh the id_token — note the chart's defaults do **not** enable these (default scope is `openid profile email groups`), so operators must opt in. As a safety net regardless of refresh configuration, `getCurrentUser()` distinguishes three states:
141141

142142
- **authenticated** — valid token → set user state.
143143
- **expired**`Authorization` header present but token missing/expired → the UI re-runs the OIDC flow (`/oauth2/start`) to mint a fresh token, guarded against redirect loops.

helm/kagent/templates/ui-deployment.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ spec:
6363
{{- if .Values.ui.auth }}
6464
- name: SSO_REDIRECT_PATH
6565
value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }}
66+
# Client components (e.g. AuthContext) can only read NEXT_PUBLIC_* vars at runtime.
67+
- name: NEXT_PUBLIC_SSO_REDIRECT_PATH
68+
value: {{ .Values.ui.auth.ssoRedirectPath | default "/oauth2/start" | quote }}
6669
{{- end }}
6770
{{- with .Values.ui.additionalForwardedHeaders }}
6871
- name: KAGENT_ADDITIONAL_FORWARDED_HEADERS

ui/src/contexts/AuthContext.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"use client";
22

3-
import React, { createContext, useContext, useEffect, useState, ReactNode } from "react";
4-
import { getCurrentUser, CurrentUser, AuthStatus } from "@/app/actions/auth";
3+
import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react";
4+
import { getCurrentUser, type CurrentUser, type AuthStatus } from "@/app/actions/auth";
55

6-
// oauth2-proxy endpoint that (re)starts the OIDC flow. Mirrors the branded
7-
// login page's SSO_REDIRECT_PATH.
8-
const SSO_REAUTH_PATH = process.env.NEXT_PUBLIC_SSO_REAUTH_PATH || "/oauth2/start";
6+
// oauth2-proxy endpoint that (re)starts the OIDC flow. Client components can only
7+
// read NEXT_PUBLIC_* env vars at runtime, so this mirrors the server-side
8+
// SSO_REDIRECT_PATH (used by the login page) via NEXT_PUBLIC_SSO_REDIRECT_PATH,
9+
// which the Helm chart injects from ui.auth.ssoRedirectPath.
10+
const SSO_REDIRECT_PATH = process.env.NEXT_PUBLIC_SSO_REDIRECT_PATH || "/oauth2/start";
911
// Guards against redirect loops if re-auth keeps returning a stale token.
1012
const REAUTH_GUARD_KEY = "kagent_reauth_attempt";
1113
const REAUTH_GUARD_WINDOW_MS = 10_000;
@@ -60,7 +62,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
6062
}
6163
sessionStorage.setItem(REAUTH_GUARD_KEY, String(Date.now()));
6264
const rd = encodeURIComponent(window.location.pathname + window.location.search);
63-
window.location.href = `${SSO_REAUTH_PATH}?rd=${rd}`;
65+
window.location.assign(`${SSO_REDIRECT_PATH}?rd=${rd}`);
6466
}, [isLoading, status]);
6567

6668
useEffect(() => {

ui/src/contexts/__tests__/AuthContext.test.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,23 @@ function setResult(result: AuthResult) {
3434
mockedGetCurrentUser.mockResolvedValue(result);
3535
}
3636

37-
// jsdom hardens window.location (non-configurable, href setter throws
38-
// "navigation not implemented"), so rather than asserting the redirect URL we
39-
// assert the observable contract: the sessionStorage re-auth guard that gates
40-
// the redirect. The guard is written immediately before window.location.href
41-
// is set, so "guard written" == "redirect attempted".
37+
// jsdom fully hardens window.location (it is non-configurable and assign is
38+
// read-only), so the redirect call (window.location.assign) cannot be spied on
39+
// here. Instead we assert the observable contract that gates the redirect: the
40+
// sessionStorage re-auth guard, written immediately before assign() is invoked
41+
// ("guard written" == "redirect attempted"). console.error is silenced to drop
42+
// jsdom's expected "Not implemented: navigation" noise from the assign() call.
4243
describe("AuthProvider re-auth behavior", () => {
44+
let consoleErrorSpy: jest.SpyInstance;
45+
4346
beforeEach(() => {
4447
jest.clearAllMocks();
4548
sessionStorage.clear();
49+
consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(() => {});
50+
});
51+
52+
afterEach(() => {
53+
consoleErrorSpy.mockRestore();
4654
});
4755

4856
it("attempts an OIDC re-auth redirect when the session is expired", async () => {

0 commit comments

Comments
 (0)