keycloakify-custom/src/App/oidc.tsx
2023-03-21 02:55:32 +01:00

214 lines
6.5 KiB
TypeScript

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 jwt_decode from "jwt-decode";
import { Evt } from "evt";
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 access a page that requires to
//be authenticated but cancel (clicks back).
doesCurrentHrefRequiresAuth: boolean;
}) => Promise<never>;
};
export type LoggedIn = {
isUserLoggedIn: true;
getAccessToken: () => string;
logout: (params: { redirectTo: "home" | "current page" }) => Promise<never>;
//If we have sent a API request to change user's email for example
//and we want that jwt_decode(oidcClient.getAccessToken()).email be the new email
//in this case we would call this method...
updateTokenInfos: () => Promise<void>;
};
}
type Params = {
url: string;
realm: string;
clientId: string;
transformUrlBeforeRedirect?: (url: string) => string;
getUiLocales?: () => string;
log?: typeof console.log;
};
async function createKeycloakOidcClient(params: Params): Promise<OidcClient> {
const {
url,
realm,
clientId,
transformUrlBeforeRedirect,
getUiLocales,
log
} = params;
const keycloakInstance = new 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 ?? (url => url))
.map(
getUiLocales === undefined ?
(url => url) :
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
});
}
let currentAccessToken = keycloakInstance.token!;
const oidcClient = id<OidcClient.LoggedIn>({
"isUserLoggedIn": true,
"getAccessToken": () => currentAccessToken,
"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>(() => { });
},
"updateTokenInfos": async () => {
await keycloakInstance.updateToken(-1);
currentAccessToken = keycloakInstance.token!;
}
});
(function callee() {
const msBeforeExpiration = jwt_decode<{ exp: number }>(currentAccessToken)["exp"] * 1000 - Date.now();
setTimeout(async () => {
log?.(`OIDC access token will expire in ${minValiditySecond} seconds, waiting for user activity before renewing`);
await Evt.merge([
Evt.from(document, "mousemove"),
Evt.from(document, "keydown")
]).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 });
}
currentAccessToken = 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 };
}