keycloakify-custom/src/App/oidc.tsx

207 lines
6.0 KiB
TypeScript
Raw Normal View History

import { useState, useContext, createContext, useEffect } from "react";
import keycloak_js from "keycloak-js";
import { id } from "tsafe/id";
import { addParamToUrl } from "powerhooks/tools/urlSearchParams";
import type { ReturnType } from "tsafe/ReturnType";
import type { Param0 } from "tsafe/Param0";
import { assert } from "tsafe/assert";
import { createKeycloakAdapter } from "keycloakify";
import type { NonPostableEvt } from "evt";
import jwt_decode from "jwt-decode";
export declare type OidcClient = OidcClient.LoggedIn | OidcClient.NotLoggedIn;
export declare namespace OidcClient {
export type NotLoggedIn = {
isUserLoggedIn: false;
login: (params: {
//To prevent infinite loop if the user click login then cancel.
doesCurrentHrefRequiresAuth: boolean;
}) => Promise<never>;
};
export type LoggedIn = {
isUserLoggedIn: true;
//NOTE: It changes when renewed, don't store it.
accessToken: string;
logout: (params: { redirectTo: "home" | "current page" }) => Promise<never>;
updateTokenInfo: () => Promise<void>;
};
}
type Params = {
url: string;
realm: string;
clientId: string;
transformUrlBeforeRedirect: (url: string) => string;
evtUserActivity: NonPostableEvt<void>;
getUiLocales: () => string;
log?: typeof console.log;
};
async function createKeycloakOidcClient(params: Params): Promise<OidcClient> {
const {
url,
realm,
clientId,
transformUrlBeforeRedirect,
evtUserActivity,
getUiLocales,
log
} = params;
const keycloakInstance = keycloak_js({ url, realm, clientId });
let redirectMethod: ReturnType<
Param0<typeof createKeycloakAdapter>["getRedirectMethod"]
> = "overwrite location.href";
const isAuthenticated = await keycloakInstance
.init({
"onLoad": "check-sso",
"silentCheckSsoRedirectUri": `${window.location.origin}/silent-sso.html`,
"responseMode": "query",
"checkLoginIframe": false,
"adapter": createKeycloakAdapter({
"transformUrlBeforeRedirect": url =>
[url].map(transformUrlBeforeRedirect).map(
url =>
addParamToUrl({
url,
"name": "ui_locales",
"value": getUiLocales()
}).newUrl
)[0],
keycloakInstance,
"getRedirectMethod": () => redirectMethod
})
})
.catch((error: Error) => error);
//TODO: Make sure that result is always an object.
if (isAuthenticated instanceof Error) {
throw isAuthenticated;
}
const login: OidcClient.NotLoggedIn["login"] = async ({
doesCurrentHrefRequiresAuth
}) => {
if (doesCurrentHrefRequiresAuth) {
redirectMethod = "location.replace";
}
await keycloakInstance.login({ "redirectUri": window.location.href });
return new Promise<never>(() => { });
};
if (!isAuthenticated) {
return id<OidcClient.NotLoggedIn>({
"isUserLoggedIn": false,
login
});
}
const oidcClient = id<OidcClient.LoggedIn>({
"isUserLoggedIn": true,
"accessToken": keycloakInstance.token!,
"logout": async ({ redirectTo }) => {
await keycloakInstance.logout({
"redirectUri": (() => {
switch (redirectTo) {
case "current page":
return window.location.href;
case "home":
return window.location.origin;
}
})()
});
return new Promise<never>(() => { });
},
"updateTokenInfo": async () => {
await keycloakInstance.updateToken(-1);
oidcClient.accessToken = keycloakInstance.token!;
}
});
(function callee() {
const msBeforeExpiration =
jwt_decode<{ exp: number }>(oidcClient.accessToken)["exp"] * 1000 - Date.now();
setTimeout(async () => {
log?.(
`OIDC access token will expire in ${minValiditySecond} seconds, waiting for user activity before renewing`
);
await evtUserActivity.waitFor();
log?.("User activity detected. Refreshing access token now");
const error = await keycloakInstance.updateToken(-1).then(
() => undefined,
(error: Error) => error
);
if (error) {
log?.("Can't refresh OIDC access token, getting a new one");
//NOTE: Never resolves
await login({ "doesCurrentHrefRequiresAuth": true });
}
oidcClient.accessToken = keycloakInstance.token!;
callee();
}, msBeforeExpiration - minValiditySecond * 1000);
})();
return oidcClient;
}
const minValiditySecond = 25;
const oidcClientContext = createContext<OidcClient | undefined>(undefined);
export function createOidcClientProvider(params: Params) {
const prOidcClient = createKeycloakOidcClient(params);
function OidcClientProvider(props: { children: React.ReactNode; }) {
const { children } = props;
const [oidcClient, setOidcClient] = useState<OidcClient | undefined>(undefined);
useEffect(() => {
prOidcClient.then(setOidcClient);
}, []);
if (oidcClient === undefined) {
return null;
}
return (
<oidcClientContext.Provider value={oidcClient}>
{children}
</oidcClientContext.Provider>
);
}
return { OidcClientProvider };
}
export function useOidcClient() {
const oidcClient = useContext(oidcClientContext);
assert(oidcClient !== undefined);
return { oidcClient };
}