Skip to content

Commit

Permalink
Update oidc-spa
Browse files Browse the repository at this point in the history
  • Loading branch information
garronej committed Mar 2, 2025
1 parent dbfe366 commit b618bf4
Show file tree
Hide file tree
Showing 35 changed files with 225 additions and 127 deletions.
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"memoizee": "^0.4.17",
"minimal-polyfills": "^2.2.3",
"mui-icons-material-lazy": "^1.0.4",
"oidc-spa": "^6.5.2",
"oidc-spa": "^6.9.4",
"onyxia-ui": "^6.2.3",
"pathe": "^1.1.2",
"powerhooks": "^1.0.19",
Expand Down
49 changes: 37 additions & 12 deletions web/src/core/adapters/oidc/oidc.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,63 @@
import type { Oidc } from "core/ports/Oidc";
import { createOidc as createOidcSpa } from "oidc-spa";
import { parseKeycloakIssuerUri } from "oidc-spa/tools/parseKeycloakIssuerUri";
import type { OidcParams, OidcParams_Partial } from "core/ports/OnyxiaApi";
import { objectKeys } from "tsafe/objectKeys";

export async function createOidc<AutoLogin extends boolean>(
params: OidcParams & {
transformUrlBeforeRedirect: (url: string) => string;
transformUrlBeforeRedirect_ui: (params: {
isKeycloak: boolean;
authorizationUrl: string;
}) => string;
autoLogin: AutoLogin;
}
): Promise<AutoLogin extends true ? Oidc.LoggedIn : Oidc> {
const {
issuerUri,
clientId,
scope_spaceSeparated,
clientSecret,
transformUrlBeforeRedirect,
audience,
transformUrlBeforeRedirect_ui,
extraQueryParams_raw,
autoLogin
} = params;

const oidc = await createOidcSpa({
issuerUri,
clientId,
__unsafe_clientSecret: clientSecret,
scopes: scope_spaceSeparated?.split(" "),
transformUrlBeforeRedirect: url => {
url = transformUrlBeforeRedirect(url);
transformUrlBeforeRedirect_next: ({ authorizationUrl, isSilent }) => {
if (!isSilent) {
authorizationUrl = transformUrlBeforeRedirect_ui({
isKeycloak:
parseKeycloakIssuerUri(oidc.params.issuerUri) !== undefined,
authorizationUrl
});
}

if (audience !== undefined) {
const url_obj = new URL(authorizationUrl);

url_obj.searchParams.set("audience", audience);

authorizationUrl = url_obj.href;
}

if (extraQueryParams_raw !== undefined) {
url += `&${extraQueryParams_raw}`;
const url_obj = new URL(authorizationUrl);
const extraUrlSearchParams = new URLSearchParams(extraQueryParams_raw);

for (const [key, value] of extraUrlSearchParams) {
url_obj.searchParams.set(key, value);
}

authorizationUrl = url_obj.href;
}

return url;
return authorizationUrl;
},
homeUrl: import.meta.env.BASE_URL,
debugLogs: false
homeUrl: import.meta.env.BASE_URL
});

if (!oidc.isUserLoggedIn) {
Expand Down Expand Up @@ -63,9 +86,11 @@ export function mergeOidcParams(params: {

for (const key of objectKeys(oidcParams_partial)) {
const value = oidcParams_partial[key];
if (value !== undefined) {
oidcParams_merged[key] = value;
if (value === undefined) {
continue;
}
// @ts-expect-error
oidcParams_merged[key] = value;
}

return oidcParams_merged;
Expand Down
3 changes: 2 additions & 1 deletion web/src/core/adapters/onyxiaApi/ApiTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ export namespace ApiTypes {
clientID: string;
extraQueryParams?: string;
scope?: string;
workaroundForGoogleClientSecret?: string;
audience?: string;
idleSessionLifetimeInSeconds?: number | string;
};
}
37 changes: 31 additions & 6 deletions web/src/core/adapters/onyxiaApi/onyxiaApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,25 @@ export function createOnyxiaApi(params: {
: id<OidcParams>({
issuerUri: data.oidcConfiguration.issuerURI,
clientId: data.oidcConfiguration.clientID,
clientSecret:
data.oidcConfiguration
.workaroundForGoogleClientSecret || undefined,
extraQueryParams_raw:
data.oidcConfiguration.extraQueryParams || undefined,
scope_spaceSeparated:
data.oidcConfiguration.scope || undefined
data.oidcConfiguration.scope || undefined,
audience: data.oidcConfiguration.audience || undefined,
idleSessionLifetimeInSeconds: (() => {
const value =
data.oidcConfiguration.idleSessionLifetimeInSeconds;

if (value === "" || value === undefined) {
return undefined;
}

if (typeof value === "number") {
return value;
}

return parseInt(value);
})()
});

const regions = data.regions.map(
Expand Down Expand Up @@ -974,8 +986,21 @@ function apiTypesOidcConfigurationToOidcParams_Partial(
return {
issuerUri: oidcConfiguration?.issuerURI || undefined,
clientId: oidcConfiguration?.clientID || undefined,
clientSecret: oidcConfiguration?.workaroundForGoogleClientSecret || undefined,
extraQueryParams_raw: oidcConfiguration?.extraQueryParams || undefined,
scope_spaceSeparated: oidcConfiguration?.scope || undefined
scope_spaceSeparated: oidcConfiguration?.scope || undefined,
audience: oidcConfiguration?.audience || undefined,
idleSessionLifetimeInSeconds: (() => {
const value = oidcConfiguration?.idleSessionLifetimeInSeconds;

if (value === "" || value === undefined) {
return undefined;
}

if (typeof value === "number") {
return value;
}

return parseInt(value);
})()
};
}
9 changes: 6 additions & 3 deletions web/src/core/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import { assert } from "tsafe/assert";

type ParamsOfBootstrapCore = {
apiUrl: string;
transformUrlBeforeRedirectToLogin: (url: string) => string;
transformUrlBeforeRedirectToLogin: (params: {
isKeycloak: boolean;
authorizationUrl: string;
}) => string;
getCurrentLang: () => Language;
disablePersonalInfosInjectionInGroup: boolean;
isCommandBarEnabledByDefault: boolean;
Expand Down Expand Up @@ -116,7 +119,7 @@ export async function bootstrapCore(

return createOidc({
...oidcParams,
transformUrlBeforeRedirect: transformUrlBeforeRedirectToLogin,
transformUrlBeforeRedirect_ui: transformUrlBeforeRedirectToLogin,
autoLogin: false
});
})();
Expand Down Expand Up @@ -219,7 +222,7 @@ export async function bootstrapCore(
oidcParams,
oidcParams_partial: deploymentRegion.vault.oidcParams
}),
transformUrlBeforeRedirect: transformUrlBeforeRedirectToLogin,
transformUrlBeforeRedirect_ui: transformUrlBeforeRedirectToLogin,
autoLogin: true
})
});
Expand Down
31 changes: 23 additions & 8 deletions web/src/core/ports/Oidc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export declare namespace Oidc {

export type LoggedIn = Common & {
isUserLoggedIn: true;
renewTokens(): Promise<void>;
renewTokens: () => Promise<void>;
getTokens: () => Promise<Tokens>;
logout: (params: { redirectTo: "home" | "current page" }) => Promise<never>;
isNewBrowserSession: boolean;
Expand All @@ -24,11 +24,26 @@ export declare namespace Oidc {
) => { unsubscribeFromAutoLogoutCountdown: () => void };
};

export type Tokens = {
accessToken: string;
idToken: string;
refreshToken: string;
refreshTokenExpirationTime: number;
decodedIdToken: Record<string, unknown>;
};
export type Tokens = Tokens.WithRefreshToken | Tokens.WithoutRefreshToken;

export namespace Tokens {
export type Common = {
accessToken: string;
accessTokenExpirationTime: number;
idToken: string;
decodedIdToken: Record<string, unknown>;
};

export type WithRefreshToken = Common & {
hasRefreshToken: true;
refreshToken: string;
refreshTokenExpirationTime: number | undefined;
};

export type WithoutRefreshToken = Common & {
hasRefreshToken: false;
refreshToken?: never;
refreshTokenExpirationTime?: never;
};
}
}
3 changes: 2 additions & 1 deletion web/src/core/ports/OnyxiaApi/OidcParams.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
export type OidcParams = {
issuerUri: string;
clientId: string;
audience: string | undefined;
extraQueryParams_raw: string | undefined;
scope_spaceSeparated: string | undefined;
clientSecret: string | undefined;
idleSessionLifetimeInSeconds: number | undefined;
};

export type OidcParams_Partial = { [P in keyof OidcParams]: OidcParams[P] | undefined };
8 changes: 5 additions & 3 deletions web/src/core/usecases/k8sCodeSnippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export const thunks = {
oidcParams_partial: region.kubernetes.oidcParams
}),
autoLogin: true,
transformUrlBeforeRedirect:
transformUrlBeforeRedirect_ui:
paramsOfBootstrapCore.transformUrlBeforeRedirectToLogin
});

Expand All @@ -153,12 +153,14 @@ export const thunks = {
actions.refreshed({
idpIssuerUrl: kubernetesOidcClient.params.issuerUri,
clientId: kubernetesOidcClient.params.clientId,
refreshToken: oidcTokens.refreshToken,
refreshToken: oidcTokens.refreshToken ?? "",
idToken: oidcTokens.idToken,
user: `${region.kubernetes.usernamePrefix ?? ""}${
userAuthentication.selectors.user(getState()).username
}`,
expirationTime: oidcTokens.refreshTokenExpirationTime
expirationTime:
oidcTokens.refreshTokenExpirationTime ??
oidcTokens.accessTokenExpirationTime
})
);
}
Expand Down
2 changes: 1 addition & 1 deletion web/src/core/usecases/launcher/thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ const privateThunks = {
lang: paramsOfBootstrapCore.getCurrentLang(),
decodedIdToken,
accessToken,
refreshToken
refreshToken: refreshToken ?? ""
},
service: {
oneTimePassword: generateRandomPassword()
Expand Down
2 changes: 1 addition & 1 deletion web/src/core/usecases/s3ConfigManagement/thunks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const protectedThunks = {
oidcParams_partial
}),
autoLogin: true,
transformUrlBeforeRedirect:
transformUrlBeforeRedirect_ui:
paramsOfBootstrapCore.transformUrlBeforeRedirectToLogin
})
);
Expand Down
39 changes: 19 additions & 20 deletions web/src/ui/App/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,26 +30,25 @@ injectCustomFontFaceIfNotAlreadyDone();
const { CoreProvider } = createCoreProvider({
apiUrl: env.ONYXIA_API_URL,
getCurrentLang: () => evtLang.state,
transformUrlBeforeRedirectToLogin: url =>
[url]
.map(injectTransferableEnvsInQueryParams)
.map(injectGlobalStatesInSearchParams)
.map(
url =>
addParamToUrl({
url,
name: onyxiaInstancePublicUrlKey,
value: `${window.location.origin}${env.PUBLIC_URL}`
}).newUrl
)
.map(
url =>
addParamToUrl({
url,
name: "ui_locales",
value: evtLang.state
}).newUrl
)[0],
transformUrlBeforeRedirectToLogin: ({ authorizationUrl, isKeycloak }) => {
if (isKeycloak) {
authorizationUrl = injectTransferableEnvsInQueryParams(authorizationUrl);
authorizationUrl = injectGlobalStatesInSearchParams(authorizationUrl);
authorizationUrl = addParamToUrl({
url: authorizationUrl,
name: onyxiaInstancePublicUrlKey,
value: `${window.location.origin}${env.PUBLIC_URL}`
}).newUrl;
}

authorizationUrl = addParamToUrl({
url: authorizationUrl,
name: "ui_locales",
value: evtLang.state
}).newUrl;

return authorizationUrl;
},
disablePersonalInfosInjectionInGroup: env.DISABLE_PERSONAL_INFOS_INJECTION_IN_GROUP,
isCommandBarEnabledByDefault: !env.DISABLE_COMMAND_BAR,
quotaWarningThresholdPercent: env.QUOTA_WARNING_THRESHOLD * 100,
Expand Down
26 changes: 2 additions & 24 deletions web/src/ui/App/Main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ import { useSplashScreen } from "onyxia-ui";
import { keyframes } from "tss-react";
import { objectKeys } from "tsafe/objectKeys";
import { pages } from "ui/pages";
import { useCore, useCoreState } from "core";
import { CircularProgress } from "onyxia-ui/CircularProgress";

type Props = {
className?: string;
Expand All @@ -17,31 +15,17 @@ export const Main = memo((props: Props) => {

const route = useRoute();

const { userAuthentication } = useCore().functions;
const { isUserLoggedIn } = useCoreState("userAuthentication", "authenticationState");

const { classes } = useStyles();

return (
<main className={className}>
<Suspense fallback={<SuspenseFallback />}>
{(() => {
for (const pageName of objectKeys(pages)) {
//You must be able to replace "home" by any other page and get no type error.
//We must be able to replace "home" by any other page and get no type error.
const page = pages[pageName as "home"];

if (page.routeGroup.has(route)) {
if (page.getDoRequireUserLoggedIn(route) && !isUserLoggedIn) {
userAuthentication.login({
doesCurrentHrefRequiresAuth: true
});
return (
<div className={classes.loginRedirect}>
<CircularProgress size={70} />
</div>
);
}

return (
<page.LazyComponent
className={classes.page}
Expand Down Expand Up @@ -70,7 +54,7 @@ function SuspenseFallback() {
return null;
}

const useStyles = tss.create({
const useStyles = tss.withName({ Main }).create({
page: {
animation: `${keyframes`
0% {
Expand All @@ -80,11 +64,5 @@ const useStyles = tss.create({
opacity: 1;
}
`} 400ms`
},
loginRedirect: {
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "100%"
}
});
Loading

0 comments on commit b618bf4

Please sign in to comment.