Skip to content

Commit

Permalink
Get rid of Keycloak-js in favor of oidc-client-ts
Browse files Browse the repository at this point in the history
  • Loading branch information
garronej committed Oct 3, 2023
1 parent 5fe169f commit 161760f
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 76 deletions.
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.8",
"@mui/material": "^5.14.8",
"oidc-client-ts": "^2.3.0",
"@reduxjs/toolkit": "^1.9.1",
"memoizee": "^0.4.14",
"zod": "^3.19.0",
Expand All @@ -52,7 +53,6 @@
"https-browserify": "^1.0.0",
"jwt-decode": "^3.1.2",
"jwt-simple": "^0.5.6",
"keycloak-js": "21.0.2",
"minimal-polyfills": "^2.2.3",
"moment": "^2.29.1",
"mustache": "^4.2.0",
Expand Down
138 changes: 77 additions & 61 deletions web/src/core/adapters/oidc/oidc.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import type { Oidc } from "../../ports/Oidc";
import keycloak_js from "keycloak-js";
import { UserManager, WebStorageStateStore } from "oidc-client-ts";
import { id } from "tsafe/id";
import { createKeycloakAdapter } from "keycloakify";
import type { Oidc } from "../../ports/Oidc";
import { decodeJwt } from "core/tools/jwt";
import type { Param0, ReturnType } from "tsafe";
import { addParamToUrl } from "powerhooks/tools/urlSearchParams";
import { assert } from "tsafe/assert";
import { addParamToUrl, retrieveParamFromUrl } from "powerhooks/tools/urlSearchParams";
import { Evt } from "evt";

export async function createOidc(params: {
Expand All @@ -19,73 +17,97 @@ export async function createOidc(params: {
const { url, realm, clientId, transformUrlBeforeRedirect, 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 =>
// prettier-ignore
[url]
.map(transformUrlBeforeRedirect)
.map(
const userManager = new UserManager({
"authority": `${url}/realms/${realm}`,
"client_id": clientId,
"redirect_uri": "" /* provided when calling login */,
"response_type": "code",
"scope": "openid profile",
"userStore": new WebStorageStateStore({ "store": window.localStorage }),
"automaticSilentRenew": false,
"silent_redirect_uri": `${window.location.origin}/silent-sso.html`
});

const login: Oidc.NotLoggedIn["login"] = async () => {
//NOTE: We know there is a extraQueryParameter option but it doesn't allow
// to control the encoding so we have to hack the global URL Class that is
// used internally by oidc-client-ts

const URL_real = window.URL;

function URL(...args: ConstructorParameters<typeof URL_real>) {
const urlInstance = new URL_real(...args);

return new Proxy(urlInstance, {
"get": (target, prop) => {
if (prop === "href") {
return [urlInstance.href].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;
}
)[0];
}

const login: Oidc.NotLoggedIn["login"] = async ({ doesCurrentHrefRequiresAuth }) => {
if (doesCurrentHrefRequiresAuth) {
redirectMethod = "location.replace";
//@ts-expect-error
return target[prop];
}
});
}

await keycloakInstance.login({ "redirectUri": window.location.href });
Object.defineProperty(window, "URL", { "value": URL });

await userManager.signinRedirect({
"redirect_uri": window.location.href,
"redirectMethod": "replace"
});
return new Promise<never>(() => {});
};

if (!isAuthenticated) {
read_successful_login_query_params: {
let url = window.location.href;

const names = ["code", "state", "session_state"];

for (const name of names) {
const result = retrieveParamFromUrl({ name, url });

if (!result.wasPresent) {
if (names.indexOf(name) === 0) {
break read_successful_login_query_params;
} else {
assert(false);
}
}

url = result.newUrl;
}

await userManager.signinRedirectCallback();

window.location.replace(url);
}

let currentAccessToken = (await userManager.getUser())?.access_token ?? "";

if (currentAccessToken === "") {
return id<Oidc.NotLoggedIn>({
"isUserLoggedIn": false,
login
});
}

assert(keycloakInstance.token !== undefined);

let currentAccessToken = keycloakInstance.token;

const oidc = id<Oidc.LoggedIn>({
"isUserLoggedIn": true,
"getAccessToken": () => ({
"accessToken": currentAccessToken,
"expirationTime": getAccessTokenExpirationTime(currentAccessToken)
}),
"logout": async ({ redirectTo }) => {
await keycloakInstance.logout({
"redirectUri": (() => {
await userManager.signoutRedirect({
"post_logout_redirect_uri": (() => {
switch (redirectTo) {
case "current page":
return window.location.href;
Expand All @@ -94,15 +116,16 @@ export async function createOidc(params: {
}
})()
});

return new Promise<never>(() => {});
},
"renewToken": async () => {
await keycloakInstance.updateToken(-1);
const user = await userManager.signinSilent({
"redirect_uri": window.location.href
});

assert(keycloakInstance.token !== undefined);
assert(user !== null);

currentAccessToken = keycloakInstance.token;
currentAccessToken = user.access_token;
}
});

Expand All @@ -122,21 +145,14 @@ export async function createOidc(params: {

log?.("User activity detected. Refreshing access token now");

const error = await keycloakInstance.updateToken(-1).then(
() => undefined,
(error: Error) => error
);

if (error) {
try {
await oidc.renewToken();
} catch {
log?.("Can't refresh OIDC access token, getting a new one");
//NOTE: Never resolves
await login({ "doesCurrentHrefRequiresAuth": true });
}

assert(keycloakInstance.token !== undefined);

currentAccessToken = keycloakInstance.token;

callee();
}, msBeforeExpiration - minValiditySecond * 1000);
})();
Expand All @@ -147,5 +163,5 @@ export async function createOidc(params: {
const minValiditySecond = 25;

function getAccessTokenExpirationTime(accessToken: string): number {
return decodeJwt<{ exp: number }>(accessToken)["exp"] * 1000;
return decodeJwt<{ exp: number }>(accessToken).exp * 1000;
}
28 changes: 14 additions & 14 deletions web/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5395,7 +5395,7 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==

base64-js@^1.0.2, base64-js@^1.3.1, base64-js@^1.5.1:
base64-js@^1.0.2, base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
Expand Down Expand Up @@ -6600,6 +6600,11 @@ crypto-browserify@^3.11.0, crypto-browserify@^3.12.0:
randombytes "^2.0.0"
randomfill "^1.0.3"

crypto-js@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.1.1.tgz#9e485bcf03521041bd85844786b83fb7619736cf"
integrity sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==

crypto-random-string@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5"
Expand Down Expand Up @@ -10610,11 +10615,6 @@ js-sdsl@^4.1.4:
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430"
integrity sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==

js-sha256@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966"
integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==

js-string-escape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
Expand Down Expand Up @@ -10767,14 +10767,6 @@ jwt-simple@^0.5.6:
resolved "https://registry.yarnpkg.com/jwt-simple/-/jwt-simple-0.5.6.tgz#3357adec55b26547114157be66748995b75b333a"
integrity sha512-40aUybvhH9t2h71ncA1/1SbtTNCVZHgsTsTgqPUxGWDmUDrXyDf2wMNQKEbdBjbf4AI+fQhbECNTV6lWxQKUzg==

[email protected]:
version "21.0.2"
resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-21.0.2.tgz#1d3c2079d3c23850df4f253a868926861c51c488"
integrity sha512-i05i3VBPhQ867EgjA+OYPlf8YUPiUwtrU2zv4j8tvZIdRvhJY8f+mp1ZvRJl/GMRb+XhJs9BDknyBMrIspwDkw==
dependencies:
base64-js "^1.5.1"
js-sha256 "^0.9.0"

keycloakify@^8.1.2:
version "8.1.2"
resolved "https://registry.yarnpkg.com/keycloakify/-/keycloakify-8.1.2.tgz#ecd8e07980bed17d2a585455c704fbc198657fe6"
Expand Down Expand Up @@ -12012,6 +12004,14 @@ obuf@^1.0.0, obuf@^1.1.2:
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==

oidc-client-ts@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/oidc-client-ts/-/oidc-client-ts-2.3.0.tgz#43c90f1f0cc3be2e4ede38b8c68642ba00bfa8f6"
integrity sha512-7RUKU+TJFQo+4X9R50IGJAIDF18uRBaFXyZn4VVCfwmwbSUhKcdDnw4zgeut3uEXkiD3NqURq+d88sDPxjf1FA==
dependencies:
crypto-js "^4.1.1"
jwt-decode "^3.1.2"

[email protected]:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
Expand Down

0 comments on commit 161760f

Please sign in to comment.