diff --git a/console/docker/generate-config.sh b/console/docker/generate-config.sh index 3f311dd4..b13088b8 100644 --- a/console/docker/generate-config.sh +++ b/console/docker/generate-config.sh @@ -33,7 +33,11 @@ window.APP_CONFIG = { VITE_OIDC_ISSUER_URL: '${VITE_OIDC_ISSUER_URL}', VITE_OIDC_CLIENT_ID: '${VITE_OIDC_CLIENT_ID}', VITE_OIDC_REDIRECT_URI: '${VITE_OIDC_REDIRECT_URI}', - VITE_OIDC_SCOPE: '${VITE_OIDC_SCOPE}' + VITE_OIDC_SCOPE: '${VITE_OIDC_SCOPE}', + VITE_PROXY_AUTH: '${VITE_PROXY_AUTH}', + VITE_PROXY_USERINFO_PATH: '${VITE_PROXY_USERINFO_PATH}', + VITE_PROXY_USER_FIELDS: '${VITE_PROXY_USER_FIELDS}', + VITE_PROXY_PRINCIPAL_FIELD: '${VITE_PROXY_PRINCIPAL_FIELD}' }; EOF diff --git a/console/src/components/layout/Header.tsx b/console/src/components/layout/Header.tsx index 68c56c0f..061ce70b 100644 --- a/console/src/components/layout/Header.tsx +++ b/console/src/components/layout/Header.tsx @@ -20,6 +20,7 @@ import { LogOut, ChevronDown, Sun, Moon, Monitor, Search } from "lucide-react" import { useAuth } from "@/hooks/useAuth" import { useCurrentUser } from "@/hooks/useCurrentUser" +import { useProxyUser } from "@/hooks/useProxyUser" import { useTheme } from "@/hooks/useTheme" import { config } from "@/lib/config" import { @@ -38,12 +39,27 @@ interface HeaderProps { export function Header({ onSearchOpen }: HeaderProps) { const { logout } = useAuth() - const { principal, principalRoles, loading } = useCurrentUser() + const { + displayName: proxyDisplayName, + principalName: proxyPrincipalName, + loading: proxyLoading, + } = useProxyUser() + const { + principal, + principalRoles, + loading: principalLoading, + } = useCurrentUser(proxyPrincipalName) const { theme, setTheme } = useTheme() - // Get display name and role + const loading = principalLoading || proxyLoading + + // Get display name and role — prefer the proxy identity when deployed behind the auth proxy const displayName = - principal?.name || principal?.properties?.displayName || principal?.properties?.name || "User" + proxyDisplayName || + principal?.name || + principal?.properties?.displayName || + principal?.properties?.name || + "User" const primaryRole = principalRoles.length > 0 ? principalRoles[0].name diff --git a/console/src/hooks/useAuth.tsx b/console/src/hooks/useAuth.tsx index cfa289b6..e85f96fd 100644 --- a/console/src/hooks/useAuth.tsx +++ b/console/src/hooks/useAuth.tsx @@ -21,6 +21,7 @@ import { createContext, useContext, useState, type ReactNode } from "react" import { toast } from "sonner" import { authApi } from "@/api/auth" import { apiClient } from "@/api/client" +import { config } from "@/lib/config" interface AuthContextType { isAuthenticated: boolean @@ -34,7 +35,7 @@ interface AuthContextType { const AuthContext = createContext(undefined) export function AuthProvider({ children }: { children: ReactNode }) { - const [isAuthenticated, setIsAuthenticated] = useState(false) + const [isAuthenticated, setIsAuthenticated] = useState(config.PROXY_AUTH) const [loading] = useState(false) const login = async (clientId: string, clientSecret: string, scope: string) => { diff --git a/console/src/hooks/useCurrentUser.tsx b/console/src/hooks/useCurrentUser.tsx index e306aea2..6621e3be 100644 --- a/console/src/hooks/useCurrentUser.tsx +++ b/console/src/hooks/useCurrentUser.tsx @@ -34,11 +34,11 @@ interface CurrentUserInfo { * Hook to fetch the current logged-in user's principal information * Decodes the JWT token to get the principal name, then fetches full details */ -export function useCurrentUser(): CurrentUserInfo { +export function useCurrentUser(principalNameOverride?: string | null): CurrentUserInfo { const token = apiClient.getAccessToken() - // Get principal name from token - const principalName = token ? getPrincipalNameFromToken(token) : null + // Use the override (e.g. proxy-provided) principal name, else fall back to the token + const principalName = principalNameOverride ?? (token ? getPrincipalNameFromToken(token) : null) // Fetch principal details const { diff --git a/console/src/hooks/useProxyUser.tsx b/console/src/hooks/useProxyUser.tsx new file mode 100644 index 00000000..53ff0f41 --- /dev/null +++ b/console/src/hooks/useProxyUser.tsx @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { useQuery } from "@tanstack/react-query" +import { config } from "@/lib/config" + +interface ProxyUser { + displayName: string | null + principalName: string | null + loading: boolean +} + +/** Helper to read a field from the proxy body */ +function pickField(body: Record, field: string): string | null { + const value = body[field.trim()] + return typeof value === "string" && value !== "" ? value : null +} + +/** + * Hook to fetch the authenticated user from the auth proxy (e.g. oauth2-proxy) + * when the console is deployed behind it (VITE_PROXY_AUTH=true). In this setup + * the browser never holds the token - the proxy does. Therefore the identity + * comes from the proxy's userinfo endpoint rather than a decoded JWT. + * + * The response body shape is not standardized across proxies, so the keys to + * read are runtime-configurable: + * - VITE_PROXY_USER_FIELDS: comma-separated priority list for the header + * display name. + * - VITE_PROXY_PRINCIPAL_FIELD: single key holding the Polaris principal + * name used to fetch principal details/roles (empty = don't fetch). + * + * Its a no-op if proxy mode is disabled. Fails silently for any non 200 / non + * JSON response (e.g. a 401 or a redirect to the IdP when the proxy is absent). + */ +export function useProxyUser(): ProxyUser { + const { data, isLoading } = useQuery | null>({ + queryKey: ["proxyUser"], + queryFn: async () => { + try { + const response = await fetch(config.PROXY_USERINFO_PATH, { + credentials: "include", + headers: { Accept: "application/json" }, + }) + if (!response.ok) { + return null + } + const contentType = response.headers.get("content-type") || "" + if (!contentType.includes("application/json")) { + return null + } + const body = await response.json() + return body && typeof body === "object" ? (body as Record) : null + } catch { + return null + } + }, + enabled: config.PROXY_AUTH, + staleTime: 5 * 60 * 1000, // Cache for 5 minutes + retry: false, + }) + + let displayName: string | null = null + if (data) { + for (const field of config.PROXY_USER_FIELDS.split(",")) { + displayName = pickField(data, field) + if (displayName) { + break + } + } + } + + const principalName = + data && config.PROXY_PRINCIPAL_FIELD ? pickField(data, config.PROXY_PRINCIPAL_FIELD) : null + + return { + displayName, + principalName, + loading: config.PROXY_AUTH && isLoading, + } +} diff --git a/console/src/lib/config.ts b/console/src/lib/config.ts index df866378..c84147cc 100644 --- a/console/src/lib/config.ts +++ b/console/src/lib/config.ts @@ -27,6 +27,10 @@ interface AppConfig { VITE_OIDC_CLIENT_ID?: string VITE_OIDC_REDIRECT_URI?: string VITE_OIDC_SCOPE?: string + VITE_PROXY_AUTH?: string + VITE_PROXY_USERINFO_PATH?: string + VITE_PROXY_USER_FIELDS?: string + VITE_PROXY_PRINCIPAL_FIELD?: string } declare global { @@ -59,4 +63,8 @@ export const config = { OIDC_CLIENT_ID: getConfig("VITE_OIDC_CLIENT_ID", ""), OIDC_REDIRECT_URI: getConfig("VITE_OIDC_REDIRECT_URI", ""), OIDC_SCOPE: getConfig("VITE_OIDC_SCOPE", "openid profile email"), + PROXY_AUTH: getConfig("VITE_PROXY_AUTH", "false") === "true", + PROXY_USERINFO_PATH: getConfig("VITE_PROXY_USERINFO_PATH", "/oauth2/userinfo"), + PROXY_USER_FIELDS: getConfig("VITE_PROXY_USER_FIELDS", "preferredUsername,user,email"), + PROXY_PRINCIPAL_FIELD: getConfig("VITE_PROXY_PRINCIPAL_FIELD", ""), }