diff --git a/e2e/mock-api/src/app/routes.auth.js b/e2e/mock-api/src/app/routes.auth.js index 3337f22b3..cc6499c7c 100644 --- a/e2e/mock-api/src/app/routes.auth.js +++ b/e2e/mock-api/src/app/routes.auth.js @@ -440,7 +440,7 @@ export default function (app) { app.get('/login', async (req, res) => { const domain = req.url.includes('localhost') ? 'localhost' : 'example.com'; - res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain }); + res.cookie('iPlanetDirectoryPro', 'abcd1234', { domain, sameSite: 'none', secure: true }); const url = new URL(`${req.protocol}://${req.headers.host}${authPaths.authorize[1]}`); url.searchParams.set('client_id', req.query.client_id); diff --git a/e2e/token-vault-app/index.html b/e2e/token-vault-app/index.html index 228359bb8..ee6442c91 100644 --- a/e2e/token-vault-app/index.html +++ b/e2e/token-vault-app/index.html @@ -51,6 +51,7 @@ +
User Logged In:
diff --git a/e2e/token-vault-app/src/main.ts b/e2e/token-vault-app/src/main.ts index 93717d47b..253650a50 100644 --- a/e2e/token-vault-app/src/main.ts +++ b/e2e/token-vault-app/src/main.ts @@ -100,6 +100,7 @@ const loggedInEl = getById('loggedInDef'); const userInfoEl = getById('userInfoDef'); const hasTokensEl = getById('hasTokensDef'); const refreshTokensEl = getById('refreshTokensDef'); +const hackerEl = getById('hacker'); /** * If the URL has state and code as query parameters, then the user @@ -153,6 +154,29 @@ refreshTokensBtn.addEventListener('click', async (event) => { console.log(res); }); +hackerEl.addEventListener('click', async () => { + console.log('in hacker function!'); + const proxyChannel = new MessageChannel(); + const proxyOrigin = 'http://localhost:5833'; + + // Create a request to a URL that is not allow-listed + const request = { url: 'https://reqres.in/api/users/2' }; + + const type = 'TVP_FETCH_RESOURCE'; + + // Grab the Proxy's iframe and post message to it + (document?.getElementById('token-vault-iframe') as HTMLIFrameElement)?.contentWindow?.postMessage( + { type, request }, + proxyOrigin, + [proxyChannel.port2], + ); + + // This is how you listen for the response from the Proxy + proxyChannel.port1.onmessage = (event) => { + console.log(event.data); // This should return error + }; +}); + loginBtn.addEventListener('click', async (event) => { console.log('Logging in...'); await TokenManager.getTokens({ diff --git a/e2e/token-vault-proxy/src/main.ts b/e2e/token-vault-proxy/src/main.ts index 7c6d424b9..51339247a 100644 --- a/e2e/token-vault-proxy/src/main.ts +++ b/e2e/token-vault-proxy/src/main.ts @@ -15,4 +15,7 @@ proxy({ }, realmPath: import.meta.env.VITE_AM_REALM, }, + proxy: { + urls: ['https://jsonplaceholder.typicode.com/*'], + }, }); diff --git a/e2e/token-vault-suites/project.json b/e2e/token-vault-suites/project.json index 40530e0c9..6d59e7f09 100644 --- a/e2e/token-vault-suites/project.json +++ b/e2e/token-vault-suites/project.json @@ -40,6 +40,6 @@ } } }, - "tags": [], + "tags": ["scope:e2e"], "implicitDependencies": ["token-vault-app", "token-vault-proxy"] } diff --git a/e2e/token-vault-suites/src/basic.test.ts b/e2e/token-vault-suites/src/basic.test.ts index 647b53cd0..08f5b0404 100644 --- a/e2e/token-vault-suites/src/basic.test.ts +++ b/e2e/token-vault-suites/src/basic.test.ts @@ -89,3 +89,27 @@ test('Test happy paths on test page', async ({ page }) => { const revokedTokens = await getTokens('http://localhost:5833', 'CentralLoginOAuthClient'); expect(revokedTokens).toBeFalsy(); }); +/* + * ensure the proxy is not called when the url is not in the allow list + * and that the proxy responds with an error + */ +test('Ensure someone cannot try to call their own url!', async ({ page }) => { + const { navigate } = asyncEvents(page); + await navigate('/'); + + expect(page.url()).toBe('http://localhost:5823/'); + + const messageArray = []; + page.on('console', (message) => messageArray.push(message.text())); + + await page.click('#hacker'); + expect( + messageArray.includes('Received TVP_FETCH_RESOURCE event from http://localhost:5823'), + ).toBe(true); + expect(messageArray.includes('Proxying https://reqres.in/api/users/2')).toBe(true); + expect( + messageArray.includes( + '{error: unrecognized_origin, message: Unrecognized origin: https://reqres.in. Please configure URLs in Proxy.}', + ), + ).toBe(true); +}); diff --git a/package-lock.json b/package-lock.json index c4ec92dea..99e2008eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,12 +5,7 @@ "packages": { "": { "name": "@forgerock/javascript-sdk", - "workspaces": [ - "e2e/*", - "packages/*", - "samples/*", - "shared/*" - ], + "workspaces": ["e2e/*", "packages/*", "samples/*", "shared/*"], "devDependencies": { "@changesets/changelog-github": "^0.4.8", "@changesets/cli": "^2.26.1", @@ -4023,14 +4018,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", "integrity": "sha512-EmwL+vUBZJ7mhFCs5lA4ZimpUH3WMAoqvOIYhVQwdIgSpHC8ImHdsRyhHAVxpDYUSm0lWvd63z0XH1IlImS2Qw==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -4039,14 +4030,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.18.tgz", "integrity": "sha512-/iq0aK0eeHgSC3z55ucMAHO05OIqmQehiGay8eP5l/5l+iEr4EIbh4/MI8xD9qRFjqzgkc0JkX0LculNC9mXBw==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -4055,14 +4042,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.18.tgz", "integrity": "sha512-x+0efYNBF3NPW2Xc5bFOSFW7tTXdAcpfEg2nXmxegm4mJuVeS+i109m/7HMiOQ6M12aVGGFlqJX3RhNdYM2lWg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -4071,29 +4054,21 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.18.tgz", "integrity": "sha512-6tY+djEAdF48M1ONWnQb1C+6LiXrKjmqjzPNPWXhu/GzOHTHX2nh8Mo2ZAmBFg0kIodHhciEgUBtcYCAIjGbjQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } }, "node_modules/@esbuild/darwin-x64": { "version": "0.17.19", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } @@ -4102,14 +4077,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.18.tgz", "integrity": "sha512-fw/ZfxfAzuHfaQeMDhbzxp9mc+mHn1Y94VDHFHjGvt2Uxl10mT4CDavHm+/L9KG441t1QdABqkVYwakMUeyLRA==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -4118,14 +4089,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.18.tgz", "integrity": "sha512-FQFbRtTaEi8ZBi/A6kxOC0V0E9B/97vPdYjY9NdawyLd4Qk5VD5g2pbWN2VR1c0xhzcJm74HWpObPszWC+qTew==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -4134,14 +4101,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.18.tgz", "integrity": "sha512-jW+UCM40LzHcouIaqv3e/oRs0JM76JfhHjCavPxMUti7VAPh8CaGSlS7cmyrdpzSk7A+8f0hiedHqr/LMnfijg==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4150,14 +4113,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.18.tgz", "integrity": "sha512-R7pZvQZFOY2sxUG8P6A21eq6q+eBv7JPQYIybHVf1XkQYC+lT7nDBdC7wWKTrbvMXKRaGudp/dzZCwL/863mZQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4166,14 +4125,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.18.tgz", "integrity": "sha512-ygIMc3I7wxgXIxk6j3V00VlABIjq260i967Cp9BNAk5pOOpIXmd1RFQJQX9Io7KRsthDrQYrtcx7QCof4o3ZoQ==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4182,14 +4137,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.18.tgz", "integrity": "sha512-bvPG+MyFs5ZlwYclCG1D744oHk1Pv7j8psF5TfYx7otCVmcJsEXgFEhQkbhNW8otDHL1a2KDINW20cfCgnzgMQ==", - "cpu": [ - "loong64" - ], + "cpu": ["loong64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4198,14 +4149,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.18.tgz", "integrity": "sha512-oVqckATOAGuiUOa6wr8TXaVPSa+6IwVJrGidmNZS1cZVx0HqkTMkqFGD2HIx9H1RvOwFeWYdaYbdY6B89KUMxA==", - "cpu": [ - "mips64el" - ], + "cpu": ["mips64el"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4214,14 +4161,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.18.tgz", "integrity": "sha512-3dLlQO+b/LnQNxgH4l9rqa2/IwRJVN9u/bK63FhOPB4xqiRqlQAU0qDU3JJuf0BmaH0yytTBdoSBHrb2jqc5qQ==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4230,14 +4173,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.18.tgz", "integrity": "sha512-/x7leOyDPjZV3TcsdfrSI107zItVnsX1q2nho7hbbQoKnmoeUWjs+08rKKt4AUXju7+3aRZSsKrJtaRmsdL1xA==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4246,14 +4185,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.18.tgz", "integrity": "sha512-cX0I8Q9xQkL/6F5zWdYmVf5JSQt+ZfZD2bJudZrWD+4mnUvoZ3TDDXtDX2mUaq6upMFv9FlfIh4Gfun0tbGzuw==", - "cpu": [ - "s390x" - ], + "cpu": ["s390x"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4262,14 +4197,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.18.tgz", "integrity": "sha512-66RmRsPlYy4jFl0vG80GcNRdirx4nVWAzJmXkevgphP1qf4dsLQCpSKGM3DUQCojwU1hnepI63gNZdrr02wHUA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -4278,14 +4209,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.18.tgz", "integrity": "sha512-95IRY7mI2yrkLlTLb1gpDxdC5WLC5mZDi+kA9dmM5XAGxCME0F8i4bYH4jZreaJ6lIZ0B8hTrweqG1fUyW7jbg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "netbsd" - ], + "os": ["netbsd"], "engines": { "node": ">=12" } @@ -4294,14 +4221,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.18.tgz", "integrity": "sha512-WevVOgcng+8hSZ4Q3BKL3n1xTv5H6Nb53cBrtzzEjDbbnOmucEVcZeGCsCOi9bAOcDYEeBZbD2SJNBxlfP3qiA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "openbsd" - ], + "os": ["openbsd"], "engines": { "node": ">=12" } @@ -4310,14 +4233,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.18.tgz", "integrity": "sha512-Rzf4QfQagnwhQXVBS3BYUlxmEbcV7MY+BH5vfDZekU5eYpcffHSyjU8T0xucKVuOcdCsMo+Ur5wmgQJH2GfNrg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "sunos" - ], + "os": ["sunos"], "engines": { "node": ">=12" } @@ -4326,14 +4245,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.18.tgz", "integrity": "sha512-Kb3Ko/KKaWhjeAm2YoT/cNZaHaD1Yk/pa3FTsmqo9uFh1D1Rfco7BBLIPdDOozrObj2sahslFuAQGvWbgWldAg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -4342,14 +4257,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.18.tgz", "integrity": "sha512-0/xUMIdkVHwkvxfbd5+lfG7mHOf2FRrxNbPiKWg9C4fFrB8H0guClmaM3BFiRUYrznVoyxTIyC/Ou2B7QQSwmw==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -4358,14 +4269,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.18.tgz", "integrity": "sha512-qU25Ma1I3NqTSHJUOKi9sAH1/Mzuvlke0ioMJRthLXKm7JiSKVwFghlGbDLOO2sARECGhja4xYfRAZNPAkooYg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -6798,14 +6705,10 @@ }, "node_modules/@nx/nx-darwin-x64": { "version": "16.3.2", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">= 10" } @@ -8339,15 +8242,11 @@ }, "node_modules/@swc/core-darwin-x64": { "version": "1.3.66", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=10" } @@ -10349,9 +10248,7 @@ "node_modules/ansi-html-community": { "version": "0.0.8", "dev": true, - "engines": [ - "node >= 0.8.0" - ], + "engines": ["node >= 0.8.0"], "license": "Apache-2.0", "bin": { "ansi-html": "bin/ansi-html" @@ -11876,9 +11773,7 @@ "version": "3.2.0", "dev": true, "license": "MIT", - "workspaces": [ - "website" - ], + "workspaces": ["website"], "dependencies": { "typanion": "^3.8.0" }, @@ -14298,14 +14193,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -14314,14 +14205,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -14330,14 +14217,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -14346,14 +14229,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } @@ -14362,14 +14241,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -14378,14 +14253,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -14394,14 +14265,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14410,14 +14277,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14426,14 +14289,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14442,14 +14301,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", - "cpu": [ - "loong64" - ], + "cpu": ["loong64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14458,14 +14313,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", - "cpu": [ - "mips64el" - ], + "cpu": ["mips64el"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14474,14 +14325,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14490,14 +14337,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14506,14 +14349,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", - "cpu": [ - "s390x" - ], + "cpu": ["s390x"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14522,14 +14361,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -14538,14 +14373,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "netbsd" - ], + "os": ["netbsd"], "engines": { "node": ">=12" } @@ -14554,14 +14385,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "openbsd" - ], + "os": ["openbsd"], "engines": { "node": ">=12" } @@ -14570,14 +14397,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "sunos" - ], + "os": ["sunos"], "engines": { "node": ">=12" } @@ -14586,14 +14409,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -14602,14 +14421,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -14618,14 +14433,10 @@ "version": "0.17.19", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -15559,9 +15370,7 @@ "node_modules/extsprintf": { "version": "1.3.0", "dev": true, - "engines": [ - "node >=0.6.0" - ], + "engines": ["node >=0.6.0"], "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -16178,9 +15987,7 @@ "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } @@ -20423,9 +20230,7 @@ }, "node_modules/jsonparse": { "version": "1.3.1", - "engines": [ - "node >= 0.2.0" - ], + "engines": ["node >= 0.2.0"], "license": "MIT" }, "node_modules/JSONStream": { @@ -22970,9 +22775,7 @@ "hasInstallScript": true, "license": "MIT", "optional": true, - "os": [ - "!win32" - ], + "os": ["!win32"], "peer": true, "dependencies": { "node-addon-api": "^3.0.0", @@ -29966,14 +29769,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.6.tgz", "integrity": "sha512-J3lwhDSXBBppSzm/LC1uZ8yKSIpExc+5T8MxrYD9KNVZG81FOAu2VF2gXi/6A/LwDDQQ+b6DpQbYlo3VwxFepQ==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -29982,14 +29781,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.6.tgz", "integrity": "sha512-pL0Ci8P9q1sWbtPx8CXbc8JvPvvYdJJQ+LO09PLFsbz3aYNdFBGWJjiHU+CaObO4Ames+GOFpXRAJZS2L3ZK/A==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -29998,14 +29793,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.6.tgz", "integrity": "sha512-hE2vZxOlJ05aY28lUpB0y0RokngtZtcUB+TVl9vnLEnY0z/8BicSvrkThg5/iI1rbf8TwXrbr2heEjl9fLf+EA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "android" - ], + "os": ["android"], "engines": { "node": ">=12" } @@ -30014,29 +29805,21 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.6.tgz", "integrity": "sha512-/tuyl4R+QhhoROQtuQj9E/yfJtZNdv2HKaHwYhhHGQDN1Teziem2Kh7BWQMumfiY7Lu9g5rO7scWdGE4OsQ6MQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } }, "node_modules/tsup/node_modules/@esbuild/darwin-x64": { "version": "0.18.6", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } @@ -30045,14 +29828,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.6.tgz", "integrity": "sha512-bq10jFv42V20Kk77NvmO+WEZaLHBKuXcvEowixnBOMkaBgS7kQaqTc77ZJDbsUpXU3KKNLQFZctfaeINmeTsZA==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -30061,14 +29840,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.6.tgz", "integrity": "sha512-HbDLlkDZqUMBQaiday0pJzB6/8Xx/10dI3xRebJBReOEeDSeS+7GzTtW9h8ZnfB7/wBCqvtAjGtWQLTNPbR2+g==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "freebsd" - ], + "os": ["freebsd"], "engines": { "node": ">=12" } @@ -30077,14 +29852,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.6.tgz", "integrity": "sha512-C+5kb6rgsGMmvIdUI7v1PPgC98A6BMv233e97aXZ5AE03iMdlILFD/20HlHrOi0x2CzbspXn9HOnlE4/Ijn5Kw==", - "cpu": [ - "arm" - ], + "cpu": ["arm"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30093,14 +29864,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.6.tgz", "integrity": "sha512-NMY9yg/88MskEZH2s4i6biz/3av+M8xY5ua4HE7CCz5DBz542cr7REe317+v7oKjnYBCijHpkzo5vU85bkXQmQ==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30109,14 +29876,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.6.tgz", "integrity": "sha512-AXazA0ljvQEp7cA9jscABNXsjodKbEcqPcAE3rDzKN82Vb3lYOq6INd+HOCA7hk8IegEyHW4T72Z7QGIhyCQEA==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30125,14 +29888,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.6.tgz", "integrity": "sha512-JjBf7TwY7ldcPgHYt9UcrjZB03+WZqg/jSwMAfzOzM5ZG+tu5umUqzy5ugH/crGI4eoDIhSOTDp1NL3Uo/05Fw==", - "cpu": [ - "loong64" - ], + "cpu": ["loong64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30141,14 +29900,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.6.tgz", "integrity": "sha512-kATNsslryVxcH1sO3KP2nnyUWtZZVkgyhAUnyTVVa0OQQ9pmDRjTpHaE+2EQHoCM5wt/uav2edrAUqbwn3tkKQ==", - "cpu": [ - "mips64el" - ], + "cpu": ["mips64el"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30157,14 +29912,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.6.tgz", "integrity": "sha512-B+wTKz+8pi7mcWXFQV0LA79dJ+qhiut5uK9q0omoKnq8yRIwQJwfg3/vclXoqqcX89Ri5Y5538V0Se2v5qlcLA==", - "cpu": [ - "ppc64" - ], + "cpu": ["ppc64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30173,14 +29924,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.6.tgz", "integrity": "sha512-h44RBLVXFUSjvhOfseE+5UxQ/r9LVeqK2S8JziJKOm9W7SePYRPDyn7MhzhNCCFPkcjIy+soCxfhlJXHXXCR0A==", - "cpu": [ - "riscv64" - ], + "cpu": ["riscv64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30189,14 +29936,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.6.tgz", "integrity": "sha512-FlYpyr2Xc2AUePoAbc84NRV+mj7xpsISeQ36HGf9etrY5rTBEA+IU9HzWVmw5mDFtC62EQxzkLRj8h5Hq85yOQ==", - "cpu": [ - "s390x" - ], + "cpu": ["s390x"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30205,14 +29948,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.6.tgz", "integrity": "sha512-Mc4EUSYwzLci77u0Kao6ajB2WbTe5fNc7+lHwS3a+vJISC/oprwURezUYu1SdWAYoczbsyOvKAJwuNftoAdjjg==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "linux" - ], + "os": ["linux"], "engines": { "node": ">=12" } @@ -30221,14 +29960,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.6.tgz", "integrity": "sha512-3hgZlp7NqIM5lNG3fpdhBI5rUnPmdahraSmwAi+YX/bp7iZ7mpTv2NkypGs/XngdMtpzljICxnUG3uPfqLFd3w==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "netbsd" - ], + "os": ["netbsd"], "engines": { "node": ">=12" } @@ -30237,14 +29972,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.6.tgz", "integrity": "sha512-aEWTdZQHtSRROlDYn7ygB8yAqtnall/UnmoVIJVqccKitkAWVVSYocQUWrBOxLEFk8XdlRouVrLZe6WXszyviA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "openbsd" - ], + "os": ["openbsd"], "engines": { "node": ">=12" } @@ -30253,14 +29984,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.6.tgz", "integrity": "sha512-uxk/5yAGpjKZUHOECtI9W+9IcLjKj+2m0qf+RG7f7eRBHr8wP6wsr3XbNbgtOD1qSpPapd6R2ZfSeXTkCcAo5g==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "sunos" - ], + "os": ["sunos"], "engines": { "node": ">=12" } @@ -30269,14 +29996,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.6.tgz", "integrity": "sha512-oXlXGS9zvNCGoAT/tLHAsFKrIKye1JaIIP0anCdpaI+Dc10ftaNZcqfLzEwyhdzFAYInXYH4V7kEdH4hPyo9GA==", - "cpu": [ - "arm64" - ], + "cpu": ["arm64"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -30285,14 +30008,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.6.tgz", "integrity": "sha512-qh7IcAHUvvmMBmoIG+V+BbE9ZWSR0ohF51e5g8JZvU08kZF58uDFL5tHs0eoYz31H6Finv17te3W3QB042GqVA==", - "cpu": [ - "ia32" - ], + "cpu": ["ia32"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -30301,14 +30020,10 @@ "version": "0.18.6", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.6.tgz", "integrity": "sha512-9UDwkz7Wlm4N9jnv+4NL7F8vxLhSZfEkRArz2gD33HesAFfMLGIGNVXRoIHtWNw8feKsnGly9Hq1EUuRkWl0zA==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "win32" - ], + "os": ["win32"], "engines": { "node": ">=12" } @@ -30528,9 +30243,7 @@ "version": "3.12.1", "dev": true, "license": "MIT", - "workspaces": [ - "website" - ] + "workspaces": ["website"] }, "node_modules/type-check": { "version": "0.4.0", @@ -31245,9 +30958,7 @@ "node_modules/verror": { "version": "1.10.0", "dev": true, - "engines": [ - "node >=0.6.0" - ], + "engines": ["node >=0.6.0"], "license": "MIT", "dependencies": { "assert-plus": "^1.0.0", @@ -32464,12 +32175,12 @@ }, "packages/javascript-sdk": { "name": "@forgerock/javascript-sdk", - "version": "4.1.2", + "version": "4.2.0", "license": "MIT" }, "packages/token-vault": { "name": "@forgerock/token-vault", - "version": "4.1.2", + "version": "4.2.0", "peerDependencies": { "@forgerock/javascript-sdk": "^4.1.2" } @@ -33202,14 +32913,10 @@ "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.18.tgz", "integrity": "sha512-Qq84ykvLvya3dO49wVC9FFCNUfSrQJLbxhoQk/TE1r6MjHo3sFF2tlJCwMjhkBVq3/ahUisj7+EpRSz0/+8+9A==", - "cpu": [ - "x64" - ], + "cpu": ["x64"], "dev": true, "optional": true, - "os": [ - "darwin" - ], + "os": ["darwin"], "engines": { "node": ">=12" } diff --git a/packages/javascript-sdk/CHANGELOG.md b/packages/javascript-sdk/CHANGELOG.md index b1d1c5060..c37dc9553 100644 --- a/packages/javascript-sdk/CHANGELOG.md +++ b/packages/javascript-sdk/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [4.2.0] - 2023-09-11 + +Features: + +- Added ability for SDK to accept a logLevel and customLogger option in the config. The default to the logger is `none` which means the SDK will no longer output to the console messages/warnings/console.error calls. + ## [4.1.2] - 2023-07-20 Features: @@ -27,6 +33,10 @@ Breaking Changes: - Dropped UMD bundle support, if you would like to use a UMD bundle it's available in 3.4 or you can produce your own by git cloning the repo and setting up the ability to do so. - Removed Event and FRUI modules +Deprecated: + +- JavaScript support configuration property deprecated. + Features: - Updated the esmodule bundle diff --git a/packages/javascript-sdk/package.json b/packages/javascript-sdk/package.json index b6756ba98..15223eef6 100644 --- a/packages/javascript-sdk/package.json +++ b/packages/javascript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@forgerock/javascript-sdk", - "version": "4.1.2", + "version": "4.2.0", "description": "ForgeRock JavaScript SDK", "author": "ForgeRock", "license": "MIT", diff --git a/packages/javascript-sdk/src/config/index.ts b/packages/javascript-sdk/src/config/index.ts index 016b5316a..f8abc824c 100644 --- a/packages/javascript-sdk/src/config/index.ts +++ b/packages/javascript-sdk/src/config/index.ts @@ -20,6 +20,7 @@ function setDefaults(options: ConfigOptions): ConfigOptions { return { ...options, oauthThreshold: options.oauthThreshold || DEFAULT_OAUTH_THRESHOLD, + logLevel: options.logLevel || 'none', }; } diff --git a/packages/javascript-sdk/src/config/interfaces.ts b/packages/javascript-sdk/src/config/interfaces.ts index d3c2d98b7..0c599f7ea 100644 --- a/packages/javascript-sdk/src/config/interfaces.ts +++ b/packages/javascript-sdk/src/config/interfaces.ts @@ -12,12 +12,27 @@ import type { ActionTypes } from './enums'; import type { FRCallbackFactory } from '../fr-auth/callbacks/factory'; import type { Tokens } from '../shared/interfaces'; +type LogLevel = 'none' | 'info' | 'warn' | 'error' | 'debug'; + interface Action { type: ActionTypes; // eslint-disable-next-line @typescript-eslint/no-explicit-any payload: any; } - +/** + * Custom Logger for logger + */ +interface LoggerFunctions< + W = (...msgs: unknown[]) => void, + E = (...msgs: unknown[]) => void, + L = (...msgs: unknown[]) => void, + I = (...msgs: unknown[]) => void, +> { + warn?: W; + error?: E; + log?: L; + info?: I; +} /** * Configuration options. */ @@ -33,6 +48,8 @@ interface ConfigOptions { tree?: string; type?: string; oauthThreshold?: number; + logLevel?: LogLevel; + logger?: LoggerFunctions; } type ConfigurablePaths = keyof CustomPathConfig; @@ -80,6 +97,7 @@ interface TokenStoreObject { */ interface ValidConfigOptions extends ConfigOptions { serverConfig: ServerConfig; + logLevel: LogLevel; } export type { @@ -87,6 +105,8 @@ export type { ConfigOptions, ConfigurablePaths, CustomPathConfig, + LogLevel, + LoggerFunctions, RequestMiddleware, RequestObj, ServerConfig, diff --git a/packages/javascript-sdk/src/fr-device/index.ts b/packages/javascript-sdk/src/fr-device/index.ts index c54653cb3..ec4dfb20a 100644 --- a/packages/javascript-sdk/src/fr-device/index.ts +++ b/packages/javascript-sdk/src/fr-device/index.ts @@ -28,6 +28,7 @@ import type { } from './interfaces'; import Collector from './collector'; import { PREFIX } from '../config/constants'; +import { FRLogger } from '../util/logger'; /** * @class FRDevice - Collects user device metadata @@ -74,7 +75,7 @@ class FRDevice extends Collector { getBrowserMeta(): { [key: string]: string } { if (typeof navigator === 'undefined') { - console.warn('Cannot collect browser metadata. navigator is not defined.'); + FRLogger.warn('Cannot collect browser metadata. navigator is not defined.'); return {}; } return this.reduceToObject(this.config.browserProps, navigator); @@ -82,7 +83,7 @@ class FRDevice extends Collector { getBrowserPluginsNames(): string { if (!(typeof navigator !== 'undefined' && navigator.plugins)) { - console.warn('Cannot collect browser plugin information. navigator.plugins is not defined.'); + FRLogger.warn('Cannot collect browser plugin information. navigator.plugins is not defined.'); return ''; } return this.reduceToString(Object.keys(navigator.plugins), navigator.plugins); @@ -90,7 +91,7 @@ class FRDevice extends Collector { getDeviceName(): string { if (typeof navigator === 'undefined') { - console.warn('Cannot collect device name. navigator is not defined.'); + FRLogger.warn('Cannot collect device name. navigator is not defined.'); return ''; } const userAgent = navigator.userAgent; @@ -116,7 +117,7 @@ class FRDevice extends Collector { getDisplayMeta(): { [key: string]: string | number | null } { if (typeof screen === 'undefined') { - console.warn('Cannot collect screen information. screen is not defined.'); + FRLogger.warn('Cannot collect screen information. screen is not defined.'); return {}; } return this.reduceToObject(this.config.displayProps, screen); @@ -124,7 +125,7 @@ class FRDevice extends Collector { getHardwareMeta(): { [key: string]: string } { if (typeof navigator === 'undefined') { - console.warn('Cannot collect OS metadata. Navigator is not defined.'); + FRLogger.warn('Cannot collect OS metadata. Navigator is not defined.'); return {}; } return this.reduceToObject(this.config.hardwareProps, navigator); @@ -134,11 +135,11 @@ class FRDevice extends Collector { const storageKey = `${PREFIX}-DeviceID`; if (!(typeof globalThis.crypto !== 'undefined' && globalThis.crypto.getRandomValues)) { - console.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.'); + FRLogger.warn('Cannot generate profile ID. Crypto and/or getRandomValues is not supported.'); return ''; } if (!localStorage) { - console.warn('Cannot store profile ID. localStorage is not supported.'); + FRLogger.warn('Cannot store profile ID. localStorage is not supported.'); return ''; } let id = localStorage.getItem(storageKey); @@ -152,18 +153,18 @@ class FRDevice extends Collector { getInstalledFonts(): string { if (typeof document === undefined) { - console.warn('Cannot collect font data. Global document object is undefined.'); + FRLogger.warn('Cannot collect font data. Global document object is undefined.'); return ''; } const canvas = document.createElement('canvas'); if (!canvas) { - console.warn('Cannot collect font data. Browser does not support canvas element'); + FRLogger.warn('Cannot collect font data. Browser does not support canvas element'); return ''; } const context = canvas.getContext && canvas.getContext('2d'); if (!context) { - console.warn('Cannot collect font data. Browser does not support 2d canvas context'); + FRLogger.warn('Cannot collect font data. Browser does not support 2d canvas context'); return ''; } const text = 'abcdefghi0123456789'; @@ -185,7 +186,9 @@ class FRDevice extends Collector { async getLocationCoordinates(): Promise> { if (!(typeof navigator !== 'undefined' && navigator.geolocation)) { - console.warn('Cannot collect geolocation information. navigator.geolocation is not defined.'); + FRLogger.warn( + 'Cannot collect geolocation information. navigator.geolocation is not defined.', + ); return Promise.resolve({}); } // eslint-disable-next-line no-async-promise-executor @@ -197,7 +200,7 @@ class FRDevice extends Collector { longitude: position.coords.longitude, }), (error) => { - console.warn( + FRLogger.warn( 'Cannot collect geolocation information. ' + error.code + ': ' + error.message, ); resolve({}); @@ -213,7 +216,7 @@ class FRDevice extends Collector { getOSMeta(): { [key: string]: string } { if (typeof navigator === 'undefined') { - console.warn('Cannot collect OS metadata. navigator is not defined.'); + FRLogger.warn('Cannot collect OS metadata. navigator is not defined.'); return {}; } return this.reduceToObject(this.config.platformProps, navigator); @@ -252,7 +255,7 @@ class FRDevice extends Collector { try { return new Date().getTimezoneOffset(); } catch (err) { - console.warn('Cannot collect timezone information. getTimezoneOffset is not defined.'); + FRLogger.warn('Cannot collect timezone information. getTimezoneOffset is not defined.'); return null; } } diff --git a/packages/javascript-sdk/src/fr-user/index.ts b/packages/javascript-sdk/src/fr-user/index.ts index e337e64e6..8ae716f47 100644 --- a/packages/javascript-sdk/src/fr-user/index.ts +++ b/packages/javascript-sdk/src/fr-user/index.ts @@ -11,6 +11,7 @@ import type { ConfigOptions } from '../config'; import type { FRStepHandler } from '../fr-auth/fr-step'; import type FRStep from '../fr-auth/fr-step'; +import { FRLogger } from '../util/logger'; import OAuth2Client from '../oauth2-client'; import SessionManager from '../session-manager'; import TokenManager from '../token-manager'; @@ -31,7 +32,7 @@ abstract class FRUser { handler: FRStepHandler, options?: ConfigOptions, ): Promise { - console.info(handler, options); // Avoid lint errors + FRLogger.info(handler, options); // Avoid lint errors throw new Error('FRUser.login() not implemented'); } @@ -46,19 +47,19 @@ abstract class FRUser { // Both invalidates the session on the server AND removes browser cookie await SessionManager.logout(options); } catch (error) { - console.warn('Session logout was not successful'); + FRLogger.warn('Session logout was not successful'); } try { // Invalidates session on the server tied to the ID Token // Needed for Express environment as session logout is unavailable await OAuth2Client.endSession(options); } catch (error) { - console.warn('OAuth endSession was not successful'); + FRLogger.warn('OAuth endSession was not successful'); } try { await OAuth2Client.revokeToken(options); } catch (error) { - console.warn('OAuth revokeToken was not successful'); + FRLogger.warn('OAuth revokeToken was not successful'); } await TokenManager.deleteTokens(); } diff --git a/packages/javascript-sdk/src/http-client/helpers.ts b/packages/javascript-sdk/src/http-client/helpers.ts index 787af20da..73ccaf200 100644 --- a/packages/javascript-sdk/src/http-client/helpers.ts +++ b/packages/javascript-sdk/src/http-client/helpers.ts @@ -22,6 +22,7 @@ import type { } from './interfaces'; import type { Tokens } from '../shared/interfaces'; import { getEndpointPath, resolve, stringify } from '../util/url'; +import { FRLogger } from '../util/logger'; export function addAuthzInfoToHeaders( init: RequestInit, @@ -152,7 +153,7 @@ export function getAdvicesFromHeader(header: string): Advices { advicesValueParsed = JSON.parse(advicesValueDecoded); return advicesValueParsed; } catch (err) { - console.error('Could not parse advices value from WWW-Authenticate header'); + FRLogger.error('Could not parse advices value from WWW-Authenticate header'); } return {}; } diff --git a/packages/javascript-sdk/src/index.ts b/packages/javascript-sdk/src/index.ts index c9a8eec19..666893f46 100644 --- a/packages/javascript-sdk/src/index.ts +++ b/packages/javascript-sdk/src/index.ts @@ -73,6 +73,7 @@ import UserManager from './user-manager'; import Deferred from './util/deferred'; import PKCE from './util/pkce'; import LocalStorage from './util/storage'; +import type { LoggerFunctions } from './config/interfaces'; export type { AuthResponse, @@ -85,6 +86,7 @@ export type { GetOAuth2TokensOptions, GetTokensOptions, IdPValue, + LoggerFunctions, MessageCreator, NameValue, OAuth2Tokens, diff --git a/packages/javascript-sdk/src/token-manager/index.ts b/packages/javascript-sdk/src/token-manager/index.ts index 9143fd6fe..f07e60438 100644 --- a/packages/javascript-sdk/src/token-manager/index.ts +++ b/packages/javascript-sdk/src/token-manager/index.ts @@ -11,6 +11,7 @@ import type { ConfigOptions } from '../config'; import Config from '../config'; import { PREFIX } from '../config/constants'; +import { FRLogger } from '../util/logger'; import type { OAuth2Tokens } from '../oauth2-client'; import OAuth2Client, { allowedErrors, ResponseType } from '../oauth2-client'; import type { StringDict, Tokens } from '../shared/interfaces'; @@ -95,7 +96,7 @@ abstract class TokenManager { await OAuth2Client.revokeToken(options); await TokenManager.deleteTokens(); } catch (error) { - console.warn('Existing tokens could not be revoked or deleted', error); + FRLogger.warn('Existing tokens could not be revoked or deleted', error); } } @@ -220,7 +221,7 @@ abstract class TokenManager { try { await TokenStorage.set(tokens); } catch (error) { - console.error('Failed to store tokens', error); + FRLogger.error('Failed to store tokens', error); } return tokens; diff --git a/packages/javascript-sdk/src/token-storage/index.ts b/packages/javascript-sdk/src/token-storage/index.ts index 2d17d01cc..e9f085724 100644 --- a/packages/javascript-sdk/src/token-storage/index.ts +++ b/packages/javascript-sdk/src/token-storage/index.ts @@ -9,6 +9,7 @@ */ import Config from '../config/index'; +import { FRLogger } from '../util/logger'; import type { TokenStoreObject } from '../config/interfaces'; import LocalStorageWrapper from './local-storage'; import SessionStorageWrapper from './session-storage'; @@ -35,7 +36,7 @@ abstract class TokenStorage { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } else if (tokenStore === 'indexedDB') { - console.warn('IndexedDB is not supported in this version.'); + FRLogger.warn('IndexedDB is not supported in this version.'); } else if (tokenStore && tokenStore.get) { // User supplied token store return await tokenStore.get(clientId); @@ -58,7 +59,7 @@ abstract class TokenStorage { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } else if (tokenStore === 'indexedDB') { - console.warn('IndexedDB is not supported in this version.'); + FRLogger.warn('IndexedDB is not supported in this version.'); } else if (tokenStore && tokenStore.set) { // User supplied token store return await tokenStore.set(clientId, tokens); @@ -81,7 +82,7 @@ abstract class TokenStorage { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore } else if (tokenStore === 'indexedDB') { - console.warn('IndexedDB is not supported in this version.'); + FRLogger.warn('IndexedDB is not supported in this version.'); } else if (tokenStore && tokenStore.remove) { // User supplied token store return await tokenStore.remove(clientId); diff --git a/packages/javascript-sdk/src/util/logger.test.ts b/packages/javascript-sdk/src/util/logger.test.ts new file mode 100644 index 000000000..34a7b3164 --- /dev/null +++ b/packages/javascript-sdk/src/util/logger.test.ts @@ -0,0 +1,321 @@ +import { FRLogger } from './logger'; +import Config from '../config'; +import { LoggerFunctions } from '../config/interfaces'; + +describe('Logger Class', () => { + it('should instantiate with default log level when one is not passed in', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + }); + expect(FRLogger.enabled()).toEqual(0); + }); + it('should instantiate with the log level that is passed in and verify if it should log', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'info', + }); + expect(FRLogger.enabled()).toEqual(75); + }); + it('should call error method if logLevel allows it', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'info', + }); + const spy = jest.spyOn(FRLogger, 'error').mockImplementation(); + FRLogger.error('test'); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + it('should call warn if logLevel allows it', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'info', + }); + const spy = jest.spyOn(FRLogger, 'warn').mockImplementation(); + FRLogger.warn('test'); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + it('should call log if logLevel allows it', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'info', + }); + const spy = jest.spyOn(FRLogger, 'log').mockImplementation(); + FRLogger.log('test'); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); + it('should not call error if logLevel does not allow it', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'none', + }); + const spy = jest.spyOn(console, 'error').mockImplementation(); + FRLogger.error('test'); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + it('should only call error when set to error', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'error', + }); + const spyError = jest.spyOn(console, 'error').mockImplementation(); + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(); + const spyInfo = jest.spyOn(console, 'info').mockImplementation(); + FRLogger.log('info'); + expect(spyInfo).not.toHaveBeenCalled(); + FRLogger.warn('test'); + FRLogger.error('error'); + expect(spyWarn).not.toHaveBeenCalled(); + expect(spyError).toHaveBeenCalled(); + spyInfo.mockRestore(); + spyError.mockRestore(); + spyWarn.mockRestore(); + }); + it('should not call warn if logLevel does not allow it', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'none', + }); + const spy = jest.spyOn(console, 'warn').mockImplementation(); + FRLogger.warn('test'); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + it('should not call log if logLevel does not allow it', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'none', + }); + const spy = jest.spyOn(console, 'log').mockImplementation(); + FRLogger.log('test'); + + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + it('should allow a custom logger', () => { + const myloggerFn: LoggerFunctions = { + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + }; + + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'debug', + logger: myloggerFn, + }); + + FRLogger.warn('test'); + expect(myloggerFn.warn).toHaveBeenCalled(); + + FRLogger.log('test'); + expect(myloggerFn.log).toHaveBeenCalled(); + + FRLogger.error('test'); + expect(myloggerFn.error).toHaveBeenCalled(); + }); + it('should allow a custom logger but not call methods that arent allowed', () => { + const myloggerFn: LoggerFunctions = { + warn: jest.fn(), + error: jest.fn(), + log: jest.fn(), + }; + + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'error', + logger: myloggerFn, + }); + + FRLogger.warn('test'); + expect(myloggerFn.warn).not.toHaveBeenCalled(); + + FRLogger.log('test'); + expect(myloggerFn.log).not.toHaveBeenCalled(); + + FRLogger.error('test'); + expect(myloggerFn.error).toHaveBeenCalled(); + }); + it('should use the hierarchy of log levels appropriately for warn', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'warn', + }); + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(); + const spyError = jest.spyOn(console, 'error').mockImplementation(); + const spyLog = jest.spyOn(console, 'log').mockImplementation(); + + FRLogger.warn('test'); + expect(console.warn).toHaveBeenCalled(); + + FRLogger.log('test'); + expect(console.log).not.toHaveBeenCalled(); + + FRLogger.error('test'); + expect(console.error).toHaveBeenCalled(); + + spyWarn.mockReset(); + spyError.mockReset(); + spyLog.mockReset(); + }); + it('should use the hierarchy of log levels appropriately for info', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'info', + }); + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(); + const spyError = jest.spyOn(console, 'error').mockImplementation(); + const spyLog = jest.spyOn(console, 'log').mockImplementation(); + + FRLogger.warn('test'); + expect(console.warn).toHaveBeenCalled(); + + FRLogger.log('test'); + expect(console.log).toHaveBeenCalled(); + + FRLogger.error('test'); + expect(console.error).toHaveBeenCalled(); + + spyWarn.mockReset(); + spyError.mockReset(); + spyLog.mockReset(); + }); + it('should use the hierarchy of log levels appropriately for debug', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'debug', + }); + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(); + const spyError = jest.spyOn(console, 'error').mockImplementation(); + const spyLog = jest.spyOn(console, 'log').mockImplementation(); + + FRLogger.warn('test'); + expect(console.warn).toHaveBeenCalled(); + + FRLogger.log('test'); + expect(console.log).toHaveBeenCalled(); + + FRLogger.error('test'); + expect(console.error).toHaveBeenCalled(); + + spyWarn.mockReset(); + spyError.mockReset(); + spyLog.mockReset(); + }); + it('should use the hierarchy of log levels appropriately for error', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'error', + }); + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(); + const spyError = jest.spyOn(console, 'error').mockImplementation(); + const spyLog = jest.spyOn(console, 'log').mockImplementation(); + + FRLogger.warn('test'); + expect(console.warn).not.toHaveBeenCalled(); + + FRLogger.log('test'); + expect(console.log).not.toHaveBeenCalled(); + + FRLogger.error('test'); + expect(console.error).toHaveBeenCalled(); + + spyWarn.mockReset(); + spyError.mockReset(); + spyLog.mockReset(); + }); + it('should use the hierarchy of log levels appropriately for info', () => { + Config.set({ + serverConfig: { + baseUrl: 'http://localhost:8080', + }, + clientId: 'test', + realmPath: 'alpha', + logLevel: 'info', + }); + const spyWarn = jest.spyOn(console, 'warn').mockImplementation(); + const spyError = jest.spyOn(console, 'error').mockImplementation(); + const spyLog = jest.spyOn(console, 'log').mockImplementation(); + const spyInfo = jest.spyOn(console, 'info').mockImplementation(); + + FRLogger.warn('test'); + expect(console.warn).toHaveBeenCalled(); + + FRLogger.log('test'); + expect(console.log).toHaveBeenCalled(); + + FRLogger.info('info'); + expect(console.info).toHaveBeenCalled(); + + FRLogger.error('test'); + expect(console.error).toHaveBeenCalled(); + + spyInfo.mockReset(); + spyWarn.mockReset(); + spyError.mockReset(); + spyLog.mockReset(); + }); +}); diff --git a/packages/javascript-sdk/src/util/logger.ts b/packages/javascript-sdk/src/util/logger.ts new file mode 100644 index 000000000..a837d9402 --- /dev/null +++ b/packages/javascript-sdk/src/util/logger.ts @@ -0,0 +1,81 @@ +import Config from '../config/index'; +import { LogLevel } from '../config/interfaces'; + +/* + * Log “levels” are inclusive of its level and the level above. + * error will log only error() + * warn will log warn(), plus everything in error level + * info will log info(), plus everything from warn level + * debug will log everything + * none will log nothing + */ +type LogLevelRating = { + ['none']: 0; + ['error']: 25; + ['warn']: 50; + ['info']: 75; + ['debug']: 100; +}; + +abstract class FRLogger { + public static enabled(): LogLevelRating[LogLevel] { + const { logLevel } = Config.get(); + /* + * Return an object + * which satisfies the LogLevelRating type + * and has a key of the current log level + * and a value of the log level rating + */ + const logLevels = { + none: 0, + error: 25, + warn: 50, + info: 75, + debug: 100, + } satisfies LogLevelRating; + + return logLevels[logLevel]; + } + static info(...msgs: unknown[]) { + const { logger } = Config.get(); + if (this.enabled() >= 50) { + if (logger && logger.info) { + logger.info(...msgs); + } else { + console.info(...msgs); + } + } + } + static warn(...msgs: unknown[]) { + const { logger } = Config.get(); + if (this.enabled() >= 50) { + if (logger && logger.warn) { + logger.warn(...msgs); + } else { + console.warn(...msgs); + } + } + } + static error(...msgs: unknown[]) { + const { logger } = Config.get(); + if (this.enabled() >= 25) { + if (logger && logger.error) { + logger.error(...msgs); + } else { + console.error(...msgs); + } + } + } + static log(...msgs: unknown[]) { + const { logger } = Config.get(); + if (this.enabled() >= 75) { + if (logger && logger.log) { + logger.log(...msgs); + } else { + console.log(...msgs); + } + } + } +} + +export { FRLogger }; diff --git a/packages/javascript-sdk/tests/integration/oauth2-client.test.ts b/packages/javascript-sdk/tests/integration/oauth2-client.test.ts index 742ccd325..1444ce105 100644 --- a/packages/javascript-sdk/tests/integration/oauth2-client.test.ts +++ b/packages/javascript-sdk/tests/integration/oauth2-client.test.ts @@ -1,6 +1,7 @@ import OAuth2Client from '../../src/oauth2-client'; import PKCE from '../../src/util/pkce'; import { ResponseType } from '../../src/oauth2-client'; +import { FRLogger } from '../../src/util/logger'; jest.mock('../../src/config', () => { return { @@ -43,7 +44,7 @@ describe('Test OAuth2Client methods', () => { verifier, }; const authorizeUrl = await OAuth2Client.createAuthorizeUrl(authorizeUrlOptions); - console.log(authorizeUrl); + FRLogger.log(authorizeUrl); // eslint-disable-next-line expect(authorizeUrl).toBe( 'https://openam.example.com/am/oauth2/realms/root/realms/alpha/authorize?client_id=OAuth2ClientID&response_type=code&scope=openid%20email%20profile&state=1234&code_challenge=wxyz&code_challenge_method=S256', diff --git a/packages/javascript-sdk/tests/integration/token-storage.test-deactivate.ts b/packages/javascript-sdk/tests/integration/token-storage.test-deactivate.ts index 64bbdc27d..c04db0ed0 100644 --- a/packages/javascript-sdk/tests/integration/token-storage.test-deactivate.ts +++ b/packages/javascript-sdk/tests/integration/token-storage.test-deactivate.ts @@ -17,6 +17,7 @@ import TokenStorage from '../../src/token-storage'; import type { ConfigOptions } from '../../src/config'; import type { Tokens } from '../../src/shared/interfaces'; import { DB_NAME, TOKEN_KEY } from '../../src/token-storage/constants'; +import { FRLogger } from '../../src/util/logger'; const config: ConfigOptions = { clientId: 'mockClientId', @@ -79,7 +80,7 @@ const getTestToken = async (): Promise => { }) .catch((err) => { token = 'error'; - console.log(err); + FRLogger.log(err); }); return token; }; @@ -95,7 +96,7 @@ describe('The TokenStorage module', () => { done(); }; deleteDatabase.onerror = (): void => { - console.log('failed to delete database'); + FRLogger.log('failed to delete database'); }; }; beforeEach(cleanUp); diff --git a/packages/token-vault/CHANGELOG.md b/packages/token-vault/CHANGELOG.md index 35c557efa..4d69ea8d0 100644 --- a/packages/token-vault/CHANGELOG.md +++ b/packages/token-vault/CHANGELOG.md @@ -1,4 +1,8 @@ -## 4.1.2 - 2023-07-24 +### [4.2.0] - 2023-09-11 + +Security: - Proxy config declaring URLs is now required and will be used to generate an allow list of origins to check again prior to fowarding a request. + +## [4.1.2] - 2023-07-24 Features: diff --git a/packages/token-vault/README.md b/packages/token-vault/README.md index 7be62ccc7..720283794 100644 --- a/packages/token-vault/README.md +++ b/packages/token-vault/README.md @@ -311,7 +311,9 @@ interceptor({ }); ``` -The `interceptor.urls` array can accept a `/*` ending to match any request from a particular root domain and path without having to declare each and every unique endpoint that's used in your app. Please note that this isn't a full glob-pattern feature, but just a single ending `*`. +Note: The `interceptor.urls` array is a required property and will also be shared with the upcoming Proxy configuration, so it's best to store this array as a global, build-time value in the project. If not provided, your Interceptor will throw an error of "Config: `config.interceptor.urls` is required". + +These urls can accept a `/*` ending to match any request from a particular root domain and path without having to declare each and every unique endpoint that's used in your app. Please note that this isn't a full glob-pattern feature, but just a single ending `*` (wildcard). #### The Proxy @@ -337,9 +339,16 @@ proxy({ }, realmPath: 'alpha', }, + proxy: { + urls: [ + /* Your protected endpoints; should be identical to `interceptor.urls` */ + ], + }, }); ``` +Note: the `proxy.urls`, which is shared with `interceptor.urls` from the Interceptor configuration, is a required property for security. If not provided, your Proxy will `throw` an error or "Config: `config.proxy.urls` is required". + ## Building the Code Token Vault requires a bit more building/bundling configuration than a "normal" JavaScript app with the SDK. This is because Token Vault requires 3 different bundles: diff --git a/packages/token-vault/package.json b/packages/token-vault/package.json index 410357fe5..3609bbcf3 100644 --- a/packages/token-vault/package.json +++ b/packages/token-vault/package.json @@ -1,6 +1,6 @@ { "name": "@forgerock/token-vault", - "version": "4.1.2", + "version": "4.2.0", "private": false, "type": "commonjs", "module": "./index.mjs", diff --git a/packages/token-vault/src/lib/proxy.ts b/packages/token-vault/src/lib/proxy.ts index 97f2e1146..201463f80 100644 --- a/packages/token-vault/src/lib/proxy.ts +++ b/packages/token-vault/src/lib/proxy.ts @@ -1,4 +1,9 @@ -import { cloneResponse, createErrorResponse, generateAmUrls } from '@shared/network'; +import { + cloneResponse, + createErrorResponse, + extractOrigins, + generateAmUrls, +} from '@shared/network'; import { EventsConfig, ProxyConfig, ServerTokens } from '@shared/types'; import { refreshTokens, storeTokens, getTokens, tokenExpiryWithinThreshold } from './token.utils'; @@ -13,6 +18,9 @@ import { refreshTokens, storeTokens, getTokens, tokenExpiryWithinThreshold } fro * }); */ export function proxy(config: ProxyConfig) { + if (!config.proxy.origin) { + throw new Error('Config: `config.proxy.origin` is required'); + } /** * Client default configuration */ @@ -45,7 +53,19 @@ export function proxy(config: ProxyConfig) { /** * Generate AM URLs */ - const urls = generateAmUrls(config?.forgerock); + const amUrlObj = generateAmUrls(config?.forgerock); + const amUrlArray = Object.keys(amUrlObj).map((key) => { + return amUrlObj[key]; + }); + + /** + * Generate origins from URLs + */ + // Throw if URLs are not declared + if (!config.proxy.urls) { + throw new Error('Config: `config.proxy.urls` is required'); + } + const allowedOrigins = extractOrigins([...config.proxy.urls, ...amUrlArray]); /** * Create the proxy iframe @@ -114,7 +134,7 @@ export function proxy(config: ProxyConfig) { clientId, refreshToken: tokens.refreshToken, scope, - url: urls.accessToken, + url: amUrlObj.accessToken, }); // Check for error and build error message @@ -174,8 +194,22 @@ export function proxy(config: ProxyConfig) { console.log(`Proxying ${event.data?.request?.url}`); const request = event.data?.request || {}; + const requestUrl = request?.url || ''; + const requestOrigin = new URL(requestUrl)?.origin; const tokens = getTokens(clientId); + /** **************************************************** + * IGNORE ALL REQUESTS TO UNRECOGNIZED ORIGINS + * Ensure request origin is allow listed; if not return early + */ + if (!requestUrl || !allowedOrigins.includes(requestOrigin)) { + responseChannel.postMessage({ + error: 'unrecognized_origin', + message: `Unrecognized origin: ${requestOrigin}. Please configure URLs in Proxy.`, + }); + return; + } + /** **************************************************** * ACCESS TOKEN ENDPOINT */ @@ -335,7 +369,7 @@ export function proxy(config: ProxyConfig) { clientId, refreshToken: tokens.refreshToken, scope, - url: urls.accessToken, + url: amUrlObj.accessToken, }); } catch (error) { // Remove the tokens from localStorage and return error diff --git a/samples/angular-todo/project.json b/samples/angular-todo/project.json index 54d1d2d7f..9aaee902b 100644 --- a/samples/angular-todo/project.json +++ b/samples/angular-todo/project.json @@ -9,7 +9,7 @@ "command": "node samples/angular-todo/set-env.mjs" }, "build": { - "executor": "@angular-devkit/build-angular:browser-esbuild", + "executor": "@nx/angular:webpack-browser", "outputs": ["{options.outputPath}"], "dependsOn": ["config"], "options": { @@ -19,7 +19,7 @@ "polyfills": "samples/angular-todo/src/polyfills.ts", "tsConfig": "samples/angular-todo/tsconfig.app.json", "inlineStyleLanguage": "scss", - "assets": ["samples/angular-todo/src/favicon.ico", "samples/angular-todo/src/assets"], + "assets": ["samples/angular-todo/src/favicon.ico", "samples/angular-todo/src/assets", "samples/angular-todo/src/callback.html"], "styles": ["samples/angular-todo/src/styles.scss"], "scripts": [] }, diff --git a/samples/angular-todo/src/app/app.component.ts b/samples/angular-todo/src/app/app.component.ts index 79ff9c8f1..f1f0724f2 100644 --- a/samples/angular-todo/src/app/app.component.ts +++ b/samples/angular-todo/src/app/app.component.ts @@ -65,11 +65,11 @@ export class AppComponent implements OnInit { Config.set({ clientId: environment.WEB_OAUTH_CLIENT, - redirectUri: `${window.location.origin}/callback`, + redirectUri: `${window.location.origin}/callback.html`, scope: 'openid profile email', serverConfig: { baseUrl: environment.AM_URL, - timeout: 30000, // 90000 or less + timeout: 3000, // 9000 or less }, realmPath: environment.REALM_PATH, tree: environment.JOURNEY_LOGIN, diff --git a/samples/angular-todo/src/app/app.module.ts b/samples/angular-todo/src/app/app.module.ts index db3f36b4c..7be4f97d4 100644 --- a/samples/angular-todo/src/app/app.module.ts +++ b/samples/angular-todo/src/app/app.module.ts @@ -49,6 +49,13 @@ import { AccountIconComponent } from './icons/account-icon/account-icon.componen import { TodoIconComponent } from './icons/todo-icon/todo-icon.component'; import { ActionIconComponent } from './icons/action-icon/action-icon.component'; import { ChoiceComponent } from './features/journey/choice/choice.component'; +import { IdentityProviderComponent } from './features/journey/identityProvider/identity-provider.component'; +import { GoogleIconComponent } from './icons/google-icon/google-icon.component'; +import { AppleIconComponent } from './icons/apple-icon/apple-icon.component'; +import { WebAuthnComponent } from './features/journey/webAuthn/webAuthn.component'; +import { ConfirmationComponent } from './features/journey/confirmation/confirmation.component'; +import { TextOutputComponent } from './features/journey/textOutput/textOutput.component'; +import { FingerPrintIconComponent } from './icons/finger-print-icon/finger-print-icon.component'; @NgModule({ declarations: [ @@ -87,6 +94,13 @@ import { ChoiceComponent } from './features/journey/choice/choice.component'; TodoIconComponent, ActionIconComponent, ChoiceComponent, + IdentityProviderComponent, + GoogleIconComponent, + AppleIconComponent, + WebAuthnComponent, + ConfirmationComponent, + TextOutputComponent, + FingerPrintIconComponent, ], imports: [BrowserModule, AppRoutingModule, FormsModule], providers: [], diff --git a/samples/angular-todo/src/app/features/journey/confirmation/confirmation.component.html b/samples/angular-todo/src/app/features/journey/confirmation/confirmation.component.html new file mode 100644 index 000000000..971a3f145 --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/confirmation/confirmation.component.html @@ -0,0 +1,21 @@ + + +
+ +
diff --git a/samples/angular-todo/src/app/features/journey/confirmation/confirmation.component.ts b/samples/angular-todo/src/app/features/journey/confirmation/confirmation.component.ts new file mode 100644 index 000000000..f04c27489 --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/confirmation/confirmation.component.ts @@ -0,0 +1,40 @@ +/* + * angular-todo-prototype + * + * confirmation.component.ts + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { ConfirmationCallback } from '@forgerock/javascript-sdk'; + +/** + * Used to display login options + */ +@Component({ + selector: 'app-confirmation', + templateUrl: './confirmation.component.html', +}) +export class ConfirmationComponent { + /** + * The callback to be represented as a confirmation to a message. + */ + @Input() callback?: ConfirmationCallback; + + /** + * The name of the callback + */ + @Input() name?: string; + + /** + * Emits a boolean representing the updated state of the checkbox + */ + @Output() updatedCallback = new EventEmitter(); + + onSetOptionValue(optionValue: Event): void { + this.updatedCallback.emit((optionValue.target as HTMLInputElement).value); + } +} diff --git a/samples/angular-todo/src/app/features/journey/form/form.component.html b/samples/angular-todo/src/app/features/journey/form/form.component.html index c4c679e35..8dbe00309 100644 --- a/samples/angular-todo/src/app/features/journey/form/form.component.html +++ b/samples/angular-todo/src/app/features/journey/form/form.component.html @@ -15,7 +15,15 @@ --> @@ -39,6 +47,12 @@ + +
+ +
+
+ + + + + + + + + + + + + + diff --git a/samples/angular-todo/src/app/features/journey/form/form.component.ts b/samples/angular-todo/src/app/features/journey/form/form.component.ts index a9fb786b4..e8352891d 100644 --- a/samples/angular-todo/src/app/features/journey/form/form.component.ts +++ b/samples/angular-todo/src/app/features/journey/form/form.component.ts @@ -8,12 +8,25 @@ * of the MIT license. See the LICENSE file for details. */ -import { OnInit } from '@angular/core'; +import { EventEmitter, OnInit, Output } from '@angular/core'; import { Component, Input } from '@angular/core'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { environment } from '../../../../environments/environment'; -import type { FRLoginFailure, FRLoginSuccess, FRStep } from '@forgerock/javascript-sdk'; -import { FRAuth, TokenManager, UserManager } from '@forgerock/javascript-sdk'; +import type { + FRCallback, + FRLoginFailure, + FRLoginSuccess, + FRStep, + IdPValue, +} from '@forgerock/javascript-sdk'; +import { + CallbackType, + FRAuth, + FRWebAuthn, + TokenManager, + UserManager, + WebAuthnStepType, +} from '@forgerock/javascript-sdk'; import { UserService } from '../../../services/user.service'; /** @@ -29,6 +42,10 @@ export class FormComponent implements OnInit { */ @Input() action?: string; + /** + * the value representing whether is a webAuthn step or not + */ + @Output() isWebAuthn = new EventEmitter(); /** * The current step awaiting user input and submission to AM */ @@ -64,9 +81,20 @@ export class FormComponent implements OnInit { */ tree?: string; - constructor(private router: Router, public userService: UserService) {} + identityProviders: IdPValue[]; + code: string; + state: string; + showWebAuthn = false; + webAuthnType: WebAuthnStepType; + constructor( + private router: Router, + public userService: UserService, + private route: ActivatedRoute, + ) {} ngOnInit(): void { + this.code = this.route.snapshot.queryParamMap.get('code'); + this.state = this.route.snapshot.queryParamMap.get('state'); this.setConfigForAction(this.action); this.nextStep(); } @@ -79,6 +107,27 @@ export class FormComponent implements OnInit { this.submittingForm = true; try { + /** ********************************************************************* + * SDK INTEGRATION POINT + * Summary: Handle selection of IdpCallbacks + * ---------------------------------------------------------------------- + * Details: If we do not initially select the IdpCallback, when pressing enter, + * local authentication will automatically be submitted. + * This allows us to submit the form with the proper selection of an + * identity provider rather than local authentication. + ********************************************************************* */ + + const selectIdPCallback = step?.getCallbacksOfType(CallbackType.SelectIdPCallback); + + if (selectIdPCallback?.length > 0) { + const nameCallBacksInputs = step?.getCallbackOfType(CallbackType.NameCallback); + const idToken2Input = nameCallBacksInputs?.getInputValue('IDToken2'); + + if (this.isIdentityProviderLogin(selectIdPCallback[0]) && idToken2Input !== '') { + selectIdPCallback[0].setInputValue('localAuthentication', 0); + } + } + /** ********************************************************************* * SDK INTEGRATION POINT * Summary: Call the SDK's next method to submit the current step. @@ -86,7 +135,13 @@ export class FormComponent implements OnInit { * Details: This calls the next method with the previous step, expecting * the next step to be returned, or a success or failure. ********************************************************************* */ - const nextStep = await FRAuth.next(step, { tree: this.tree }); + + let nextStep; + if (this.code && this.state) { + nextStep = await FRAuth.resume(window.location.href); + } else { + nextStep = await FRAuth.next(step, { tree: this.tree }); + } /** ******************************************************************* * SDK INTEGRATION POINT @@ -162,6 +217,21 @@ export class FormComponent implements OnInit { handleStep(step?: FRStep) { this.step = step; + /** + * Since WebAuthn is being handled as its own unique component, it is necessary to check if we have WebAuthn type step. + * if so an output event is emitted to the log-in component to switch the login icon and render the webAuhtn component into the form component. + */ + const webAuthnType = FRWebAuthn.getWebAuthnStepType(step); + if (webAuthnType !== WebAuthnStepType.None) { + this.showWebAuthn = true; + this.webAuthnType = webAuthnType; + this.isWebAuthn.emit(true); + } + + const redirectCallback = step?.getCallbacksOfType(CallbackType.RedirectCallback); + if (redirectCallback?.length > 0) { + FRAuth.redirect(step); + } this.setConfigForAction(this.action); if (step?.getHeader()) { @@ -177,13 +247,13 @@ export class FormComponent implements OnInit { switch (action) { case 'login': { this.title = 'Sign In'; - this.buttonText = 'Sign In'; + this.buttonText = 'Submit'; this.tree = environment.JOURNEY_LOGIN; break; } case 'register': { this.title = 'Sign Up'; - (this.buttonText = 'Register'), (this.tree = environment.JOURNEY_REGISTER); + (this.buttonText = 'Submit'), (this.tree = environment.JOURNEY_REGISTER); break; } default: { @@ -194,4 +264,23 @@ export class FormComponent implements OnInit { } } } + + isIdentityProviderLogin(callback: FRCallback): boolean { + return callback + ?.getOutputByName('providers', []) + .filter((provider) => provider.provider !== 'localAuthentication').length > 0 + ? true + : false; + } + + requiresUserInput(callbacks: FRCallback[]): boolean { + return callbacks.filter((callback) => { + return ( + callback.getType() === CallbackType.ConfirmationCallback || + callback.getType() === CallbackType.SelectIdPCallback + ); + }).length > 0 + ? false + : true; + } } diff --git a/samples/angular-todo/src/app/features/journey/identityProvider/identity-provider.component.html b/samples/angular-todo/src/app/features/journey/identityProvider/identity-provider.component.html new file mode 100644 index 000000000..d28ce60bf --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/identityProvider/identity-provider.component.html @@ -0,0 +1,14 @@ +
+ +
diff --git a/samples/angular-todo/src/app/features/journey/identityProvider/identity-provider.component.ts b/samples/angular-todo/src/app/features/journey/identityProvider/identity-provider.component.ts new file mode 100644 index 000000000..6dc4390c9 --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/identityProvider/identity-provider.component.ts @@ -0,0 +1,47 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import SelectIdPCallback, { + IdPValue, +} from 'packages/javascript-sdk/src/fr-auth/callbacks/select-idp-callback'; +import { IdpNames } from '../../../shared/idps.types'; + +@Component({ + selector: 'app-identity-provider', + templateUrl: './identity-provider.component.html', +}) +export class IdentityProviderComponent implements OnInit { + /** + * The callback to be represented as select input + */ + @Input() callback?: SelectIdPCallback; + + /** + * The name of the callback + */ + @Input() name?: string; + + @Output() updatedCallback = new EventEmitter(); + + identityProviders: IdPValue[]; + + getDisplayName(idp: IdPValue): IdpNames { + switch (idp.uiConfig.buttonDisplayName) { + case 'Google': + return 'Google'; + case 'Facebook': + return 'Facebook'; + case 'Apple': + return 'Apple'; + default: + return 'Google'; + } + } + ngOnInit(): void { + this.identityProviders = this.callback + .getProviders() + .filter((provider) => provider.provider !== 'localAuthentication'); + } + + onSetProvider(provider: string): void { + this.updatedCallback.emit(provider); + } +} diff --git a/samples/angular-todo/src/app/features/journey/textOutput/textOutput.component.html b/samples/angular-todo/src/app/features/journey/textOutput/textOutput.component.html new file mode 100644 index 000000000..3d4dc0a16 --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/textOutput/textOutput.component.html @@ -0,0 +1,13 @@ + + + diff --git a/samples/angular-todo/src/app/features/journey/textOutput/textOutput.component.ts b/samples/angular-todo/src/app/features/journey/textOutput/textOutput.component.ts new file mode 100644 index 000000000..51615bb76 --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/textOutput/textOutput.component.ts @@ -0,0 +1,16 @@ +/* + * angular-todo-prototype + * + * textOutput.component.ts + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Component } from '@angular/core'; +@Component({ + selector: 'app-textOutput', + templateUrl: './textOutput.component.html', +}) +export class TextOutputComponent {} diff --git a/samples/angular-todo/src/app/features/journey/webAuthn/webAuthn.component.html b/samples/angular-todo/src/app/features/journey/webAuthn/webAuthn.component.html new file mode 100644 index 000000000..45d93555a --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/webAuthn/webAuthn.component.html @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + diff --git a/samples/angular-todo/src/app/features/journey/webAuthn/webAuthn.component.ts b/samples/angular-todo/src/app/features/journey/webAuthn/webAuthn.component.ts new file mode 100644 index 000000000..505034e1e --- /dev/null +++ b/samples/angular-todo/src/app/features/journey/webAuthn/webAuthn.component.ts @@ -0,0 +1,122 @@ +/* + * angular-todo-prototype + * + * webAuthn.component.ts + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Component, Input, OnInit } from '@angular/core'; +import { + FRAuth, + FRLoginFailure, + FRLoginSuccess, + FRStep, + TokenManager, + UserManager, + FRWebAuthn, + WebAuthnStepType, +} from '@forgerock/javascript-sdk'; +import { UserService } from '../../../services/user.service'; +import { Router } from '@angular/router'; + +/** + * Used to handle webAuthn journey + */ +@Component({ + selector: 'app-webAuthn', + templateUrl: './webAuthn.component.html', +}) +export class WebAuthnComponent implements OnInit { + @Input() webAuthnType: WebAuthnStepType; + @Input() step: FRStep; + message: string; + header: string; + success: FRLoginSuccess; + failure: FRLoginFailure; + + constructor(private router: Router, public userService: UserService) {} + + async ngOnInit(): Promise { + try { + /** ********************************************************************* + * SDK INTEGRATION POINT + * Summary: Handle type of webAuthn + * ---------------------------------------------------------------------- + * Details: Depending on the type of webAuthn being handled, we set the values + * for the message and header of the loading spinner and call for the respective method. + * Finally with the result of the request, we handle the next step by calling the next function. + ********************************************************************* */ + switch (this.webAuthnType) { + case WebAuthnStepType.Registration: + this.message = 'Your device will be used to verify your identity'; + this.header = 'Registering your device'; + this.step = await FRWebAuthn.register(this.step); + break; + + case WebAuthnStepType.Authentication: + this.message = 'Use your device to verify your identity'; + this.header = 'Verifying your identity'; + this.step = await FRWebAuthn.authenticate(this.step); + break; + } + } catch (err) { + console.log('Error in WebAuthn was caught, forwarding to server', err); + } + + let nextStep; + try { + nextStep = await FRAuth.next(this.step); + } catch (err) { + console.log('Error in calling `/authenticate`', err); + } + + switch (nextStep.type) { + case 'LoginFailure': + this.handleFailure(nextStep); + break; + case 'LoginSuccess': + this.handleSuccess(nextStep); + break; + default: + this.handleFailure(); + } + } + + handleFailure(failure?: FRLoginFailure) { + this.failure = failure; + } + + async handleSuccess(success?: FRLoginSuccess) { + this.success = success; + try { + /** ********************************************************************* + * SDK INTEGRATION POINT + * Summary: Get OAuth/OIDC tokens with Authorization Code Flow w/PKCE. + * ---------------------------------------------------------------------- + * Details: Since we have successfully authenticated the user, we can now + * get the OAuth2/OIDC tokens. We are passing the `forceRenew` option to + * ensure we get fresh tokens, regardless of existing tokens. + ************************************************************************* */ + await TokenManager.getTokens({ forceRenew: true }); + + /** ********************************************************************* + * SDK INTEGRATION POINT + * Summary: Call the user info endpoint for some basic user data. + * ---------------------------------------------------------------------- + * Details: This is an OAuth2 call that returns user information with a + * valid access token. This is optional and only used for displaying + * user info in the UI. + ********************************************************************* */ + const info = await UserManager.getCurrentUser(); + this.userService.info = info; + this.userService.isAuthenticated = true; + + this.router.navigateByUrl('/'); + } catch (err) { + console.error(err); + } + } +} diff --git a/samples/angular-todo/src/app/icons/apple-icon/apple-icon.component.html b/samples/angular-todo/src/app/icons/apple-icon/apple-icon.component.html new file mode 100644 index 000000000..e1b3cba3b --- /dev/null +++ b/samples/angular-todo/src/app/icons/apple-icon/apple-icon.component.html @@ -0,0 +1,49 @@ + + + + + + + + + + diff --git a/samples/angular-todo/src/app/icons/apple-icon/apple-icon.component.ts b/samples/angular-todo/src/app/icons/apple-icon/apple-icon.component.ts new file mode 100644 index 000000000..576914fb0 --- /dev/null +++ b/samples/angular-todo/src/app/icons/apple-icon/apple-icon.component.ts @@ -0,0 +1,19 @@ +/* + * angular-todo-prototype + * + * apple-icon.component.ts + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-apple-icon', + templateUrl: './apple-icon.component.html', +}) +export class AppleIconComponent { + @Input() size = '24px'; +} diff --git a/samples/angular-todo/src/app/icons/finger-print-icon/finger-print-icon.component.html b/samples/angular-todo/src/app/icons/finger-print-icon/finger-print-icon.component.html new file mode 100644 index 000000000..bce5d9cbd --- /dev/null +++ b/samples/angular-todo/src/app/icons/finger-print-icon/finger-print-icon.component.html @@ -0,0 +1,32 @@ + + + + + + + + + diff --git a/samples/angular-todo/src/app/icons/finger-print-icon/finger-print-icon.component.ts b/samples/angular-todo/src/app/icons/finger-print-icon/finger-print-icon.component.ts new file mode 100644 index 000000000..c60ec0221 --- /dev/null +++ b/samples/angular-todo/src/app/icons/finger-print-icon/finger-print-icon.component.ts @@ -0,0 +1,19 @@ +/* + * angular-todo-prototype + * + * finger-print-icon.component.ts + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-finger-print-icon', + templateUrl: './finger-print-icon.component.html', +}) +export class FingerPrintIconComponent { + @Input() size = '72px'; +} diff --git a/samples/angular-todo/src/app/icons/google-icon/google-icon.component.html b/samples/angular-todo/src/app/icons/google-icon/google-icon.component.html new file mode 100644 index 000000000..95a7242c6 --- /dev/null +++ b/samples/angular-todo/src/app/icons/google-icon/google-icon.component.html @@ -0,0 +1,23 @@ + + + + + + diff --git a/samples/angular-todo/src/app/icons/google-icon/google-icon.component.ts b/samples/angular-todo/src/app/icons/google-icon/google-icon.component.ts new file mode 100644 index 000000000..4d267c1b6 --- /dev/null +++ b/samples/angular-todo/src/app/icons/google-icon/google-icon.component.ts @@ -0,0 +1,19 @@ +/* + * angular-todo-prototype + * + * google-icon.component.ts + * + * Copyright (c) 2021 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-google-icon', + templateUrl: './google-icon.component.html', +}) +export class GoogleIconComponent { + @Input() size = '24px'; +} diff --git a/samples/angular-todo/src/app/shared/idps.types.ts b/samples/angular-todo/src/app/shared/idps.types.ts new file mode 100644 index 000000000..1d23fb0a8 --- /dev/null +++ b/samples/angular-todo/src/app/shared/idps.types.ts @@ -0,0 +1,16 @@ +/* + * @forgerock/javascript-sdk + * + * idps.types.ts + * + * Copyright (c) 2020 ForgeRock. All rights reserved. + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/** + * Types of identity providers + */ +type IdpNames = 'Google' | 'Apple' | 'Facebook'; + +export { IdpNames }; diff --git a/samples/angular-todo/src/app/utilities/loading/loading.component.html b/samples/angular-todo/src/app/utilities/loading/loading.component.html index e9006c5e5..0738b7cf5 100644 --- a/samples/angular-todo/src/app/utilities/loading/loading.component.html +++ b/samples/angular-todo/src/app/utilities/loading/loading.component.html @@ -13,7 +13,10 @@ - + + {{ header }} + + {{ message }}

diff --git a/samples/angular-todo/src/app/utilities/loading/loading.component.ts b/samples/angular-todo/src/app/utilities/loading/loading.component.ts index c8d9b71d7..6d8f5c331 100644 --- a/samples/angular-todo/src/app/utilities/loading/loading.component.ts +++ b/samples/angular-todo/src/app/utilities/loading/loading.component.ts @@ -18,6 +18,10 @@ import { Component, Input } from '@angular/core'; templateUrl: './loading.component.html', }) export class LoadingComponent { + /** + * The header to be displayed with the spinner + */ + @Input() header?: string; /** * The message to be displayed with the spinner */ diff --git a/samples/angular-todo/src/app/views/login/login.component.html b/samples/angular-todo/src/app/views/login/login.component.html index 26340393c..41d313df8 100644 --- a/samples/angular-todo/src/app/views/login/login.component.html +++ b/samples/angular-todo/src/app/views/login/login.component.html @@ -11,13 +11,14 @@
-
- + + +
- +

Don’t have an account? @@ -26,3 +27,7 @@

+ + + + diff --git a/samples/angular-todo/src/app/views/login/login.component.ts b/samples/angular-todo/src/app/views/login/login.component.ts index 71ad4bc0d..8a00566d4 100644 --- a/samples/angular-todo/src/app/views/login/login.component.ts +++ b/samples/angular-todo/src/app/views/login/login.component.ts @@ -17,4 +17,10 @@ import { Component } from '@angular/core'; selector: 'app-login', templateUrl: './login.component.html', }) -export class LoginComponent {} +export class LoginComponent { + isWebAuthn = false; + + onSetIsWebAuthn(isWebAuthn: boolean): void { + this.isWebAuthn = isWebAuthn; + } +} diff --git a/samples/angular-todo/src/app/views/register/register.component.html b/samples/angular-todo/src/app/views/register/register.component.html index 205bf3a09..151ecdf62 100644 --- a/samples/angular-todo/src/app/views/register/register.component.html +++ b/samples/angular-todo/src/app/views/register/register.component.html @@ -14,10 +14,12 @@
- + + +
- +

Already have an account? @@ -26,3 +28,7 @@

+ + + + diff --git a/samples/angular-todo/src/app/views/register/register.component.ts b/samples/angular-todo/src/app/views/register/register.component.ts index 7a3eb1938..3b8938e1d 100644 --- a/samples/angular-todo/src/app/views/register/register.component.ts +++ b/samples/angular-todo/src/app/views/register/register.component.ts @@ -17,4 +17,10 @@ import { Component } from '@angular/core'; selector: 'app-register', templateUrl: './register.component.html', }) -export class RegisterComponent {} +export class RegisterComponent { + isWebAuthn = false; + + onSetIsWebAuthn(isWebAuthn: boolean): void { + this.isWebAuthn = isWebAuthn; + } +} diff --git a/samples/angular-todo/src/callback.html b/samples/angular-todo/src/callback.html new file mode 100644 index 000000000..ba3027e0a --- /dev/null +++ b/samples/angular-todo/src/callback.html @@ -0,0 +1,4 @@ + + +

OK

+ diff --git a/samples/angular-todo/src/environments/environment.ts b/samples/angular-todo/src/environments/environment.ts index 76a568c75..d4799a922 100644 --- a/samples/angular-todo/src/environments/environment.ts +++ b/samples/angular-todo/src/environments/environment.ts @@ -1,5 +1,5 @@ export const environment = { - AM_URL: 'https://openam-crbrl-01.forgeblocks.com/am', + AM_URL: 'https://openam-sdks.forgeblocks.com/am/', REALM_PATH: 'alpha', WEB_OAUTH_CLIENT: 'WebOAuthClient', JOURNEY_LOGIN: 'Login', diff --git a/samples/angular-todo/src/styles/_custom-styles.scss b/samples/angular-todo/src/styles/_custom-styles.scss index 38c6b537a..601f82b38 100644 --- a/samples/angular-todo/src/styles/_custom-styles.scss +++ b/samples/angular-todo/src/styles/_custom-styles.scss @@ -1,8 +1,8 @@ /* * angular-todo-prototype - * + * * _custom-styles.scss - * + * * Copyright (c) 2021 ForgeRock. All rights reserved. * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -226,6 +226,15 @@ body { button[type='submit'] { padding: 0.75rem 1.25rem; } + + button { + padding: 0.75rem 0.75rem; + } +} + +.cstm_form-confirmation-select { + margin-bottom: 0.5rem; + transition: background-color 0.5s; } .cstm_form-floating > .cstm_form-select { padding-bottom: $form-select-padding-y; @@ -411,3 +420,49 @@ body { .text-white .cstm_dropdown-actions:focus { fill: $gray-200; } + +/** ********************************************* + * Identity provider styling + */ + +.google-login { + background-color: #fff; + color: #757575; + border-color: #ddd; + + &:hover { + color: #6d6d6d; + background-color: #eee; + border-color: #ccc; + } +} + +.apple-login { + background-color: #000000; + color: #ffffff; + border-color: #000000; + + &:hover { + background-color: #000000; + color: #ffffff; + border-color: #000000; + } +} +.facebook-login { + background-color: #3b5998; + border-color: #3b5998; + color: white; + + &:hover { + background-color: #334b7d; + border-color: #334b7d; + color: white; + } +} +app-identity-provider button { + border-radius: 8px; + width: 100%; + margin-top: 10px; + margin-bottom: 10px; + height: 3rem; +} diff --git a/samples/angular-todo/tsconfig.editor.json b/samples/angular-todo/tsconfig.editor.json index 1bf3c0a74..acab8eff1 100644 --- a/samples/angular-todo/tsconfig.editor.json +++ b/samples/angular-todo/tsconfig.editor.json @@ -2,7 +2,8 @@ "extends": "./tsconfig.json", "include": ["**/*.ts"], "compilerOptions": { - "types": ["jest", "node"] + "types": ["jest", "node"], + "moduleResolution": "node" }, "exclude": ["jest.config.ts"] } diff --git a/samples/angular-todo/tsconfig.json b/samples/angular-todo/tsconfig.json index baf0ad97d..3550ee568 100644 --- a/samples/angular-todo/tsconfig.json +++ b/samples/angular-todo/tsconfig.json @@ -25,7 +25,8 @@ "noUnusedParameters": false, "target": "es2017", "module": "es2020", - "lib": ["es2018", "dom"] + "lib": ["es2018", "dom"], + "moduleResolution": "node" }, "angularCompilerOptions": { "strictInjectionParameters": true, diff --git a/shared/network/src/lib/network.utilities.test.ts b/shared/network/src/lib/network.utilities.test.ts index fd65dce04..8d3eebb2e 100644 --- a/shared/network/src/lib/network.utilities.test.ts +++ b/shared/network/src/lib/network.utilities.test.ts @@ -1,6 +1,7 @@ import { createErrorResponse, evaluateUrlForInterception, + extractOrigins, generateAmUrls, getBaseUrl, getEndpointPath, @@ -17,12 +18,14 @@ describe('Test network utility functions', () => { const url = 'https://example.com'; expect(evaluateUrlForInterception(url, urls)).toBe(true); }); + // Test evaluateUrlForInterception with non-matching URL it('evaluateUrlForInterception should return false for non-matching URLs', () => { const urls = ['https://example.com', 'https://example.com/*']; const url = 'https://example.org'; expect(evaluateUrlForInterception(url, urls)).toBe(false); }); + // Test evaluateUrlForInterception with matching URL containing blob it('evaluateUrlForInterception should return true for matching URLs with blob', () => { const urls = ['https://example.com', 'https://example.com/*']; @@ -30,6 +33,25 @@ describe('Test network utility functions', () => { expect(evaluateUrlForInterception(url, urls)).toBe(true); }); + // Test extractOrigins + it('extractOrigins should return an array of unique origins from array of URLs', () => { + const expected = [ + 'https://example.com', + 'http://example.com', + 'https://example.com:8443', + 'https://my.forgeblocks.com', + ]; + const urls = [ + 'https://example.com/a', + 'http://example.com/b', + 'https://example.com:8443/c', + 'https://example.com/d', + 'http://example.com/e', + 'https://my.forgeblocks.com/am', + ]; + expect(extractOrigins(urls)).toStrictEqual(expected); + }); + // Test createErrorResponse with `fetch_error` type it('createErrorResponse should return error response', () => { const error = new Error('Test error'); @@ -142,7 +164,7 @@ describe('Test network utility functions', () => { expect(realmUrlPath).toBe('realms/root/realms/alpha'); }); // Test getRealmUrlPath with /alpha with trailing slash - it('getRealmUrlPath should return realm URL path without being affected by trailing slash', () => { + it('getRealmUrlPath should return realm URL path w/o being affected by trailing slash', () => { const realmUrlPath = getRealmUrlPath('alpha/'); expect(realmUrlPath).toBe('realms/root/realms/alpha'); }); diff --git a/shared/network/src/lib/network.utilities.ts b/shared/network/src/lib/network.utilities.ts index e39ba1d6a..d5b59c67b 100644 --- a/shared/network/src/lib/network.utilities.ts +++ b/shared/network/src/lib/network.utilities.ts @@ -103,6 +103,22 @@ export function evaluateUrlForInterception(url: string, urls: string[]) { return false; } +/** **************************************************************** + * @function extractOrigins - Extract a set of origins from URLs + * @param {string[]} urls - array of urls + * @returns {string[]} - array of origins + */ +export function extractOrigins(urls: string[]): string[] { + const origins: Set = new Set(); + + urls.forEach((url) => { + const { origin } = new URL(url); + origins.add(origin); + }); + + return Array.from(origins); +} + /** **************************************************************** * @function generateAmUrls - Generate the URLs for interception * @param forgerockConfig - The ForgeRock config object diff --git a/shared/types/src/lib/config.types.ts b/shared/types/src/lib/config.types.ts index 455c779a6..f05835e4c 100644 --- a/shared/types/src/lib/config.types.ts +++ b/shared/types/src/lib/config.types.ts @@ -58,6 +58,7 @@ export type BaseConfig = { origin: string; path?: string; redact?: ('access_token' | 'refresh_token' | 'id_token')[]; + urls?: string[]; }; }; @@ -90,10 +91,21 @@ export type ForgeRockConfig = FRConfig & ConfigOptions; /** **************************************************************** * Convert the BaseConfig to all optional. Then, modify the result * specifically for the Proxy's config needs. + * + * TODO: The below could use some work; just trying to reuse as much as possible */ -type ProxyConfigInit = Partial; -export interface ProxyConfig extends ProxyConfigInit { - app: BaseConfig['app']; - forgerock: BaseConfig['forgerock']; - proxy?: BaseConfig['proxy']; +type AppConfigInit = BaseConfig['app']; +type ForgeRockConfigInit = BaseConfig['forgerock']; +// Pluck out the irrelevant props for Proxy configuration +type ProxyConfigBase = Omit; +// Make only the `urls` property required. +interface ProxyConfigBaseRequired extends ProxyConfigBase { + urls: ProxyConfigBase['urls']; } +// Rebuild config specifically for Proxy configuration +export type ProxyConfig = { + app: AppConfigInit; + events?: Partial; + forgerock: ForgeRockConfigInit; + proxy: ProxyConfigBaseRequired; +}; diff --git a/shared/workers/src/lib/interceptor.test.ts b/shared/workers/src/lib/interceptor.test.ts new file mode 100644 index 000000000..c25ffb4a5 --- /dev/null +++ b/shared/workers/src/lib/interceptor.test.ts @@ -0,0 +1,7 @@ +import { interceptor } from './interceptor'; + +describe('interceptor', () => { + it('should error when no urls are passed in', () => { + expect(() => interceptor({} as any)).toThrow('Config: `config.interceptor.urls` is required'); + }); +}); diff --git a/shared/workers/src/lib/interceptor.ts b/shared/workers/src/lib/interceptor.ts index 4198c8807..90e4a5f44 100644 --- a/shared/workers/src/lib/interceptor.ts +++ b/shared/workers/src/lib/interceptor.ts @@ -25,7 +25,7 @@ declare const self: ServiceWorkerGlobalScope; export function interceptor(config: InterceptorConfig) { // Report back if no additional URLs are provided if (!config.interceptor?.urls?.length) { - console.warn('No URLs provided for Token Vault interception.'); + throw new Error('Config: `config.interceptor.urls` is required'); } const fetchEventName = config?.events?.fetch || 'TVP_FETCH_RESOURCE'; const urls = generateUrlsToIntercept(config);