diff --git a/frontend-config.json b/frontend-config.json index 6f5907d1..2bc53ad4 100644 --- a/frontend-config.json +++ b/frontend-config.json @@ -2,5 +2,10 @@ "backendUrl": "http://localhost:3000", "gatewayUrl": "https://gateway.ui.dev-core.mcpd.shoot.canary.k8s-hana.ondemand.com/kubernetes/graphql", "landscape": "LOCAL", - "documentationBaseUrl": "http://localhost:3000" + "documentationBaseUrl": "http://localhost:3000", + "oidcConfig": { + "clientId": "clientId", + "issuerUrl": "issuer-url", + "scopes": [] + } } \ No newline at end of file diff --git a/src/context/AuthProviderOnboarding.tsx b/src/context/AuthProviderOnboarding.tsx index fb160d84..570e3cbb 100644 --- a/src/context/AuthProviderOnboarding.tsx +++ b/src/context/AuthProviderOnboarding.tsx @@ -1,26 +1,36 @@ -import { ReactNode, use } from 'react'; -import { AuthProvider, AuthProviderProps } from 'react-oidc-context'; -import { useFrontendConfig } from './FrontendConfigContext.tsx'; -import { LoadCrateKubeConfig } from '../lib/oidc/crate.ts'; +import { ReactNode } from 'react'; +import { AuthProvider } from 'react-oidc-context'; +import { OIDCConfig, useFrontendConfig } from './FrontendConfigContext.tsx'; +import { WebStorageStateStore } from "oidc-client-ts"; +import { AuthProviderProps } from "react-oidc-context"; + interface AuthProviderOnboardingProps { children?: ReactNode; } -// Promise needs to be cached -// https://react.dev/blog/2024/12/05/react-19#use-does-not-support-promises-created-in-render -const fetchAuthPromiseCache = new Map>(); - export function AuthProviderOnboarding({ children, }: AuthProviderOnboardingProps) { - const { backendUrl } = useFrontendConfig(); - - const fetchAuthConfigPromise = - fetchAuthPromiseCache.get(backendUrl) ?? LoadCrateKubeConfig(backendUrl); - fetchAuthPromiseCache.set(backendUrl, fetchAuthConfigPromise); - - const authConfig = use(fetchAuthConfigPromise); + const { oidcConfig } = useFrontendConfig(); + const authConfig = buildAuthProviderConfig(oidcConfig); return {children}; } + +function buildAuthProviderConfig(oidcConfig: OIDCConfig) { + const userStore = new WebStorageStateStore({ store: window.localStorage }); + + const props: AuthProviderProps = { + authority: oidcConfig.issuerUrl, + client_id: oidcConfig.clientId, + redirect_uri: window.location.origin, + scope: oidcConfig.scopes.join(' '), + userStore: userStore, + automaticSilentRenew: false, // we show a window instead that asks the user to renew the token + onSigninCallback: () => { + window.history.replaceState({}, document.title, window.location.pathname); + }, + }; + return props; +} \ No newline at end of file diff --git a/src/context/FrontendConfigContext.tsx b/src/context/FrontendConfigContext.tsx index bdfd94df..3005076f 100644 --- a/src/context/FrontendConfigContext.tsx +++ b/src/context/FrontendConfigContext.tsx @@ -1,5 +1,6 @@ import { ReactNode, createContext, use } from 'react'; import { DocLinkCreator } from '../lib/shared/links'; +import { z } from 'zod'; export enum Landscape { Live = 'LIVE', @@ -9,18 +10,16 @@ export enum Landscape { Local = 'LOCAL', } -interface FrontendConfigContextProps { - backendUrl: string; - gatewayUrl: string; - landscape?: Landscape; - documentationBaseUrl: string; + + +interface FrontendConfigContextType extends FrontendConfig { links: DocLinkCreator; } export const FrontendConfigContext = - createContext(null); + createContext(null); -const fetchPromise = fetch('/frontend-config.json').then((res) => res.json()); +const fetchPromise = fetch('/frontend-config.json').then((res) => res.json()).then((data) => validateAndCastFrontendConfig(data)); interface FrontendConfigProviderProps { children: ReactNode; @@ -31,14 +30,10 @@ export function FrontendConfigProvider({ }: FrontendConfigProviderProps) { const config = use(fetchPromise); const docLinks = new DocLinkCreator(config.documentationBaseUrl); - const value: FrontendConfigContextProps = { + const value: FrontendConfigContextType = { links: docLinks, - backendUrl: config.backendUrl, - gatewayUrl: config.gatewayUrl, - landscape: config.landscape, - documentationBaseUrl: config.documentationBaseUrl, + ...config, }; - return ( {children} ); @@ -54,3 +49,27 @@ export const useFrontendConfig = () => { } return context; }; + +const OidcConfigSchema = z.object({ + clientId: z.string(), + issuerUrl: z.string(), + scopes: z.array(z.string()), +}); +export type OIDCConfig = z.infer; + +const FrontendConfigSchema = z.object({ + backendUrl: z.string(), + gatewayUrl: z.string(), + documentationBaseUrl: z.string(), + oidcConfig: OidcConfigSchema, + landscape: z.optional(z.nativeEnum(Landscape)), +}); +type FrontendConfig = z.infer; + +function validateAndCastFrontendConfig(config: unknown): FrontendConfig { + try { + return FrontendConfigSchema.parse(config); + } catch (error) { + throw new Error(`Invalid frontend config: ${error}`); + } +} \ No newline at end of file diff --git a/src/lib/oidc/crate.ts b/src/lib/oidc/crate.ts deleted file mode 100644 index 4a1cdcf8..00000000 --- a/src/lib/oidc/crate.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { AuthProviderProps } from 'react-oidc-context'; -import { GetAuthPropsForCurrentContext } from './shared.ts'; - -export function LoadCrateKubeConfig( - backendUrl: string, -): Promise { - const uri = backendUrl + '/.well-known/openmcp/kubeconfig'; - - return fetch(uri) - .then((res) => res.text()) - .then((data) => { - const authprops = GetAuthPropsForCurrentContext(data); - return authprops; - }) - .catch((error) => { - console.error(error); - throw new Error('Failed to load kubeconfig' + error); - }); -}