From eb9c197cea07385bc9c9c73da58e5db93d1184d2 Mon Sep 17 00:00:00 2001 From: TomJKing Date: Tue, 3 Dec 2024 09:27:54 +0000 Subject: [PATCH] Keycloak JS upgrade Version 26 of the 'keycloak-js' module has introduced siginificant changes. See here for the upgrade guide: https://www.keycloak.org/docs/latest/upgrading/index.html#keycloak-js --- app/configuration/ApplicationConfig.scala | 5 ++- app/views/partials/frontEndInputs.scala.html | 3 ++ app/viewsapi/FrontEndInfo.scala | 5 ++- conf/application.base.conf | 2 + npm/package-lock.json | 45 +++---------------- npm/package.json | 2 +- npm/src/auth/index.ts | 21 +++++---- npm/src/auth/session-timeout.ts | 11 ++--- npm/src/index.ts | 28 +++++++++--- npm/tsconfig.json | 2 +- .../CheckPageForStaticElements.scala | 6 ++- test/testUtils/FrontEndTestHelper.scala | 5 ++- 12 files changed, 71 insertions(+), 64 deletions(-) diff --git a/app/configuration/ApplicationConfig.scala b/app/configuration/ApplicationConfig.scala index cb10fecd3..059a01007 100644 --- a/app/configuration/ApplicationConfig.scala +++ b/app/configuration/ApplicationConfig.scala @@ -12,7 +12,10 @@ class ApplicationConfig @Inject() (configuration: Configuration) { get("consignmentapi.url"), get("environment"), get("region"), - get("upload.url") + get("upload.url"), + get("auth.url"), + get("auth.clientId"), + get("auth.realm") ) val numberOfItemsOnViewTransferPage: Int = configuration.get[Int]("viewTransfers.numberOfItemsPerPage") diff --git a/app/views/partials/frontEndInputs.scala.html b/app/views/partials/frontEndInputs.scala.html index e5c683e17..9d0fcf8ff 100644 --- a/app/views/partials/frontEndInputs.scala.html +++ b/app/views/partials/frontEndInputs.scala.html @@ -5,3 +5,6 @@ + + + diff --git a/app/viewsapi/FrontEndInfo.scala b/app/viewsapi/FrontEndInfo.scala index adb56dc70..2e94e952b 100644 --- a/app/viewsapi/FrontEndInfo.scala +++ b/app/viewsapi/FrontEndInfo.scala @@ -4,5 +4,8 @@ case class FrontEndInfo( apiUrl: String, stage: String, region: String, - uploadUrl: String + uploadUrl: String, + authUrl: String, + clientId: String, + realm: String ) diff --git a/conf/application.base.conf b/conf/application.base.conf index 01a1f9133..d6582bb4b 100644 --- a/conf/application.base.conf +++ b/conf/application.base.conf @@ -7,6 +7,8 @@ pekko.actor.warn-about-java-serializer-usage = "off" auth.secret = ${AUTH_SECRET} read.auth.secret = ${READ_AUTH_SECRET} auth.url=${AUTH_URL} +auth.clientId="tdr-fe" +auth.realm="tdr" consignmentapi.url = ${consignmentapi.domain}"/graphql" diff --git a/npm/package-lock.json b/npm/package-lock.json index d5e241868..f89b6ba8e 100644 --- a/npm/package-lock.json +++ b/npm/package-lock.json @@ -19,7 +19,7 @@ "copyfiles": "^2.4.1", "events": "^3.3.0", "graphql": "^16.9.0", - "keycloak-js": "25.0.6", + "keycloak-js": "26.0.4", "minify": "^11.4.1", "npm-run-all": "^4.1.5", "sass": "^1.80.4", @@ -12090,11 +12090,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-sha256": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", - "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -12243,22 +12238,10 @@ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "engines": { - "node": ">=18" - } - }, "node_modules/keycloak-js": { - "version": "25.0.6", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.6.tgz", - "integrity": "sha512-Km+dc+XfNvY6a4az5jcxTK0zPk52ns9mAxLrHj7lF3V+riVYvQujfHmhayltJDjEpSOJ4C8a57LFNNKnNnRP2g==", - "dependencies": { - "js-sha256": "^0.11.0", - "jwt-decode": "^4.0.0" - } + "version": "26.0.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.0.4.tgz", + "integrity": "sha512-MBZCLYihYawrUDPjYvryfxWXJgquA5Kr3muWyIigWkNdIP8SBupiuuyKpZlzOMYqdk1eegG1OX0R3qqt1KUKdg==" }, "node_modules/keyv": { "version": "4.5.4", @@ -25348,11 +25331,6 @@ "resolved": "https://registry.npmjs.org/jose/-/jose-5.9.6.tgz", "integrity": "sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==" }, - "js-sha256": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.0.tgz", - "integrity": "sha512-6xNlKayMZvds9h1Y1VWc0fQHQ82BxTXizWPEtEeGvmOUYpBRy4gbWroHLpzowe6xiQhHpelCQiE7HEdznyBL9Q==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -25460,19 +25438,10 @@ "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, - "jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==" - }, "keycloak-js": { - "version": "25.0.6", - "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-25.0.6.tgz", - "integrity": "sha512-Km+dc+XfNvY6a4az5jcxTK0zPk52ns9mAxLrHj7lF3V+riVYvQujfHmhayltJDjEpSOJ4C8a57LFNNKnNnRP2g==", - "requires": { - "js-sha256": "^0.11.0", - "jwt-decode": "^4.0.0" - } + "version": "26.0.4", + "resolved": "https://registry.npmjs.org/keycloak-js/-/keycloak-js-26.0.4.tgz", + "integrity": "sha512-MBZCLYihYawrUDPjYvryfxWXJgquA5Kr3muWyIigWkNdIP8SBupiuuyKpZlzOMYqdk1eegG1OX0R3qqt1KUKdg==" }, "keyv": { "version": "4.5.4", diff --git a/npm/package.json b/npm/package.json index fc5ffb83a..e1a2e0eaa 100644 --- a/npm/package.json +++ b/npm/package.json @@ -51,7 +51,7 @@ "copyfiles": "^2.4.1", "events": "^3.3.0", "graphql": "^16.9.0", - "keycloak-js": "25.0.6", + "keycloak-js": "26.0.4", "minify": "^11.4.1", "npm-run-all": "^4.1.5", "sass": "^1.80.4", diff --git a/npm/src/auth/index.ts b/npm/src/auth/index.ts index 8f956de96..9e12984c2 100644 --- a/npm/src/auth/index.ts +++ b/npm/src/auth/index.ts @@ -1,12 +1,15 @@ import Keycloak from "keycloak-js" import { IKeycloakTokenParsed } from "../upload" +import {IFrontEndInfo} from "../index"; -export const getKeycloakInstance: () => Promise< - Keycloak.KeycloakInstance | Error -> = async () => { - const keycloakInstance: Keycloak.KeycloakInstance = new Keycloak( - `${window.location.origin}/keycloak.json` - ) +export const getKeycloakInstance: (frontEndInfo: IFrontEndInfo) => Promise< + Keycloak | Error +> = async (frontEndInfo: IFrontEndInfo) => { + const keycloakInstance = new Keycloak({ + url: `${frontEndInfo.authUrl}`, + realm: `${frontEndInfo.realm}`, + clientId: `${frontEndInfo.clientId}` + }) const errorHandlingModule = await import("../errorhandling") try { const authenticated = await keycloakInstance.init({ @@ -38,7 +41,7 @@ const isRefreshTokenExpired: ( } export const scheduleTokenRefresh: ( - keycloak: Keycloak.KeycloakInstance, + keycloak: Keycloak, cookiesUrl: string, idleSessionMinValiditySecs?: number ) => void = (keycloak, cookiesUrl, idleSessionMinValiditySecs = 60) => { @@ -64,7 +67,7 @@ export const scheduleTokenRefresh: ( } export const refreshOrReturnToken: ( - keycloak: Keycloak.KeycloakInstance, + keycloak: Keycloak, tokenMinValidityInSecs?: number ) => Promise = async ( keycloak, @@ -74,7 +77,7 @@ export const refreshOrReturnToken: ( if (isRefreshTokenExpired(keycloak.refreshTokenParsed)) { const errorHandlingModule = await import("../errorhandling") const error = new errorHandlingModule.LoggedOutError( - keycloak.createLoginUrl(), + await keycloak.createLoginUrl(), "Refresh token has expired: User is logged out" ) errorHandlingModule.handleUploadError(error) diff --git a/npm/src/auth/session-timeout.ts b/npm/src/auth/session-timeout.ts index c0b6fb2f2..21a84606d 100644 --- a/npm/src/auth/session-timeout.ts +++ b/npm/src/auth/session-timeout.ts @@ -1,20 +1,21 @@ import Keycloak from "keycloak-js" +import {IFrontEndInfo} from "../index"; const timeoutDialog: HTMLDialogElement | null = document.querySelector(".timeout-dialog") -export const initialiseSessionTimeout = async (): Promise => { +export const initialiseSessionTimeout: (frontEndInfo: IFrontEndInfo) => Promise = async (frontEndInfo: IFrontEndInfo): Promise => { const authModule = await import("./index") const errorHandlingModule = await import("../errorhandling") - const keycloak = await authModule.getKeycloakInstance() + const keycloak = await authModule.getKeycloakInstance(frontEndInfo) const now: () => number = () => Math.round(new Date().getTime() / 1000) //Set timeToShowDialog to how many seconds from expiry you want the dialog log box to appear const timeToShowDialog = 300 - await setInterval(async () => { + setInterval(async () => { if (!errorHandlingModule.isError(keycloak)) { const timeUntilExpire = keycloak.refreshTokenParsed!.exp! - now() if (timeUntilExpire < 0) { - keycloak.logout() + await keycloak.logout() } else if (timeUntilExpire < timeToShowDialog) { await showModal(keycloak) } @@ -28,7 +29,7 @@ const showModal = async (keycloak: Keycloak): Promise => { if (timeoutDialog && !timeoutDialog.open) { timeoutDialog.showModal() if (extendTimeout) { - await extendTimeout.addEventListener("click", async (ev) => { + extendTimeout.addEventListener("click", async (ev) => { ev.preventDefault() await refreshToken(keycloak) }) diff --git a/npm/src/index.ts b/npm/src/index.ts index 7676349e4..b082cccbe 100644 --- a/npm/src/index.ts +++ b/npm/src/index.ts @@ -14,8 +14,11 @@ window.onload = async function () { export interface IFrontEndInfo { apiUrl: string uploadUrl: string + authUrl: string stage: string region: string + clientId: string + realm: string } const getFrontEndInfo: () => IFrontEndInfo | Error = () => { @@ -26,12 +29,21 @@ const getFrontEndInfo: () => IFrontEndInfo | Error = () => { document.querySelector(".region") const uploadUrlElement: HTMLInputElement | null = document.querySelector(".upload-url") - if (apiUrlElement && stageElement && regionElement && uploadUrlElement) { + const authUrlElement: HTMLInputElement | null = + document.querySelector(".auth-url") + const clientIdElement: HTMLInputElement | null = + document.querySelector(".client-id") + const realmElement: HTMLInputElement | null = + document.querySelector(".realm") + if (apiUrlElement && stageElement && regionElement && uploadUrlElement && authUrlElement && clientIdElement && realmElement) { return { apiUrl: apiUrlElement.value, stage: stageElement.value, region: regionElement.value, - uploadUrl: uploadUrlElement.value + uploadUrl: uploadUrlElement.value, + authUrl: authUrlElement.value, + clientId: clientIdElement.value, + realm: realmElement.value } } else { return Error("The front end information is missing") @@ -63,7 +75,7 @@ export const renderModules = async () => { const errorHandlingModule = await import("./errorhandling") if (!errorHandlingModule.isError(frontEndInfo)) { const authModule = await import("./auth") - const keycloak = await authModule.getKeycloakInstance() + const keycloak = await authModule.getKeycloakInstance(frontEndInfo) if (!errorHandlingModule.isError(keycloak)) { const metadataUploadModule = await import("./clientfilemetadataupload") const clientFileProcessing = @@ -91,7 +103,7 @@ export const renderModules = async () => { const errorHandlingModule = await import("./errorhandling") if (!errorHandlingModule.isError(frontEndInfo)) { const authModule = await import("./auth") - const keycloak = await authModule.getKeycloakInstance() + const keycloak = await authModule.getKeycloakInstance(frontEndInfo) if (!errorHandlingModule.isError(keycloak)) { const isJudgmentUser = keycloak.tokenParsed?.judgment_user const checksModule = await import("./checks") @@ -122,7 +134,7 @@ export const renderModules = async () => { const errorHandlingModule = await import("./errorhandling") if (!errorHandlingModule.isError(frontEndInfo)) { const authModule = await import("./auth") - const keycloak = await authModule.getKeycloakInstance() + const keycloak = await authModule.getKeycloakInstance(frontEndInfo) if (!errorHandlingModule.isError(keycloak)) { const checksModule = await import("./checks") const resultOrError = @@ -139,8 +151,12 @@ export const renderModules = async () => { } if (timeoutDialog) { + const frontEndInfo = getFrontEndInfo() const sessionTimeoutModule = await import("./auth/session-timeout") - await sessionTimeoutModule.initialiseSessionTimeout() + const errorHandlingModule = await import("./errorhandling") + if (!errorHandlingModule.isError(frontEndInfo)) { + await sessionTimeoutModule.initialiseSessionTimeout(frontEndInfo) + } } if (fileSelectionTree) { const trees: NodeListOf = diff --git a/npm/tsconfig.json b/npm/tsconfig.json index 4410f8835..0ebbb68f6 100644 --- a/npm/tsconfig.json +++ b/npm/tsconfig.json @@ -8,7 +8,7 @@ "allowSyntheticDefaultImports": true, "esModuleInterop": true, "sourceMap": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "strictFunctionTypes": true }, "include": ["src/*"], diff --git a/test/testUtils/CheckPageForStaticElements.scala b/test/testUtils/CheckPageForStaticElements.scala index 7761cf83e..5524e5b3b 100644 --- a/test/testUtils/CheckPageForStaticElements.scala +++ b/test/testUtils/CheckPageForStaticElements.scala @@ -98,7 +98,11 @@ class CheckPageForStaticElements() { """ | | - |""".stripMargin + | + | + | + | + |""".stripMargin ) } } diff --git a/test/testUtils/FrontEndTestHelper.scala b/test/testUtils/FrontEndTestHelper.scala index 876b1f9cb..31666960e 100644 --- a/test/testUtils/FrontEndTestHelper.scala +++ b/test/testUtils/FrontEndTestHelper.scala @@ -871,7 +871,10 @@ trait FrontEndTestHelper extends PlaySpec with MockitoSugar with Injecting with "https://mock-api-url.com/graphql", "mockStage", "mockRegion", - "https://mock-upload-url.com" + "https://mock-upload-url.com", + "https://mock-auth-url.com", + "mockClientId", + "mockRealm" ) ) frontEndInfoConfiguration