diff --git a/jest.test.config.js b/jest.test.config.js index 7af3316dc9..377d054a13 100644 --- a/jest.test.config.js +++ b/jest.test.config.js @@ -5,6 +5,12 @@ process.env.ENABLE_NEW_JSX_TRANSFORM = 'true'; */ module.exports = { preset: '@commercetools-frontend/jest-preset-mc-app/typescript', + globals: { + // This is required for the `jose` library to work in the test environment. + // We use it in the packages-backend/express package. + // Reference: https://github.com/jestjs/jest/issues/4422#issuecomment-770274099 + Uint8Array: Uint8Array, + }, moduleDirectories: [ 'application-templates', 'packages', diff --git a/packages-backend/express/package.json b/packages-backend/express/package.json index 41e939b0bd..0d9a6fec89 100644 --- a/packages-backend/express/package.json +++ b/packages-backend/express/package.json @@ -29,7 +29,7 @@ "devDependencies": { "@tsconfig/node16": "^16.1.1", "@types/jsonwebtoken": "^9.0.2", - "jose": "2.0.7", + "jose": "5.4.0", "msw": "0.49.3" } } diff --git a/packages-backend/express/src/middlewares/fixtures/jwt-token.ts b/packages-backend/express/src/middlewares/fixtures/jwt-token.ts index ac0b9a6e40..fccb25c8ba 100644 --- a/packages-backend/express/src/middlewares/fixtures/jwt-token.ts +++ b/packages-backend/express/src/middlewares/fixtures/jwt-token.ts @@ -1,19 +1,49 @@ -import { JWT, JWK, JWKS } from 'jose'; +import { + exportJWK, + generateKeyPair, + type KeyLike, + SignJWT, + type JWK, +} from 'jose'; -const keyRS256 = JWK.generateSync('RSA', 2048, { use: 'sig', alg: 'RS256' }); +let keyRS256: KeyLike; +let jwksStore: { keys: JWK[] }; -const jwksStore = new JWKS.KeyStore([keyRS256]); +async function initialize() { + // Generate RSA key pair with 2048 bits for the RS256 algorithm + const { publicKey, privateKey } = await generateKeyPair('RS256', { + modulusLength: 2048, + }); + keyRS256 = privateKey; -const createToken = (options: { issuer: string; audience: string }) => - JWT.sign( - { - sub: 'user-id', - iss: options.issuer, - aud: options.audience, - [`${options.issuer}/claims/project_key`]: 'project-key', - }, - keyRS256, - { algorithm: 'RS256' } - ); + // Export the public key to JWK format + const publicJWK: JWK = await exportJWK(publicKey); -export { jwksStore, createToken }; + // Add the necessary properties for the JWKS + publicJWK.use = 'sig'; // Signature + publicJWK.alg = 'RS256'; // Algorithm + publicJWK.kid = 'example-key-id'; // Key ID + + jwksStore = { + keys: [publicJWK], + }; +} + +const createToken = (options: { issuer: string; audience: string }) => { + if (!keyRS256) { + throw new Error( + 'Key not initialized. Please call the "initialize" function first.' + ); + } + + return new SignJWT({ + [`${options.issuer}/claims/project_key`]: 'project-key', + }) + .setAudience(options.audience) + .setIssuer(options.issuer) + .setProtectedHeader({ alg: 'RS256' }) + .setSubject('user-id') + .sign(keyRS256); +}; + +export { initialize, jwksStore, createToken }; diff --git a/packages-backend/express/src/middlewares/session-middleware.spec.ts b/packages-backend/express/src/middlewares/session-middleware.spec.ts index 953b28be9e..cb3421688b 100644 --- a/packages-backend/express/src/middlewares/session-middleware.spec.ts +++ b/packages-backend/express/src/middlewares/session-middleware.spec.ts @@ -31,11 +31,12 @@ function waitForSessionMiddleware( afterEach(() => { mockServer.resetHandlers(); }); -beforeAll(() => +beforeAll(async () => { + await fixtureJWTToken.initialize(); mockServer.listen({ onUnhandledRequest: 'error', - }) -); + }); +}); afterAll(() => mockServer.close()); describe.each` @@ -53,12 +54,12 @@ describe.each` beforeEach(() => { mockServer.use( rest.get(`${issuer}/.well-known/jwks.json`, (_req, res, ctx) => - res(ctx.json(fixtureJWTToken.jwksStore.toJWKS())) + res(ctx.json(fixtureJWTToken.jwksStore)) ) ); }); - function setupTest(options?: { + async function setupTest(options?: { middlewareOptions?: Record; requestOptions?: Record; }) { @@ -67,13 +68,14 @@ describe.each` issuer: cloudIdentifier, ...options?.middlewareOptions, }); + const token = await fixtureJWTToken.createToken({ + issuer, + audience: 'http://test-server/foo/bar', + }); const fakeRequest = { method: 'GET', headers: { - authorization: `Bearer ${fixtureJWTToken.createToken({ - issuer, - audience: 'http://test-server/foo/bar', - })}`, + authorization: `Bearer ${token}`, // The following headers are validated as they are expected to be present // in the incoming request. // To ensure we can correctly read the header values no matter if the @@ -92,7 +94,8 @@ describe.each` } it('should verify the token and attach the session info to the request', async () => { - const { sessionMiddleware, fakeRequest, fakeResponse } = setupTest(); + const { sessionMiddleware, fakeRequest, fakeResponse } = + await setupTest(); await waitForSessionMiddleware( sessionMiddleware, @@ -108,7 +111,7 @@ describe.each` }); it('should resolve the original url externally when a resolver is provided (using lambda v2)', async () => { - const { sessionMiddleware, fakeRequest, fakeResponse } = setupTest({ + const { sessionMiddleware, fakeRequest, fakeResponse } = await setupTest({ middlewareOptions: { getRequestUrl: (request: TMockAWSLambdaRequestV2) => { return `${request.rawPath}${ @@ -137,7 +140,7 @@ describe.each` }); it('should fail if incoming request does not contain expected URL params and no urlProvider is provided', async () => { - const { sessionMiddleware, fakeRequest, fakeResponse } = setupTest({ + const { sessionMiddleware, fakeRequest, fakeResponse } = await setupTest({ requestOptions: { originalUrl: undefined, rawPath: '/foo/bar', @@ -155,7 +158,7 @@ describe.each` }); it('should fail if the resolved request URI does not have a leading "/"', async () => { - const { sessionMiddleware, fakeRequest, fakeResponse } = setupTest({ + const { sessionMiddleware, fakeRequest, fakeResponse } = await setupTest({ middlewareOptions: { getRequestUrl: () => `foo/bar`, // <-- missing leading "/" }, @@ -170,12 +173,13 @@ describe.each` if (!cloudIdentifier.startsWith('http')) { it('should infer cloud identifier from custom HTTP header instead of given "mcApiUrl"', async () => { - const { sessionMiddleware, fakeRequest, fakeResponse } = setupTest({ - middlewareOptions: { - issuer: 'https://mc-api.another-ct-test.com', // This value should not matter - inferIssuer: true, - }, - }); + const { sessionMiddleware, fakeRequest, fakeResponse } = + await setupTest({ + middlewareOptions: { + issuer: 'https://mc-api.another-ct-test.com', // This value should not matter + inferIssuer: true, + }, + }); await waitForSessionMiddleware( sessionMiddleware, @@ -221,13 +225,14 @@ describe('when issuer is not a valid URL', () => { }); describe('when "X-MC-API-Cloud-Identifier" is missing', () => { it('should throw a validation error', async () => { + const token = await fixtureJWTToken.createToken({ + issuer: CLOUD_IDENTIFIERS.GCP_EU, + audience: 'http://test-server/foo/bar', + }); const fakeRequest = { method: 'GET', headers: { - authorization: `Bearer ${fixtureJWTToken.createToken({ - issuer: CLOUD_IDENTIFIERS.GCP_EU, - audience: 'http://test-server/foo/bar', - })}`, + authorization: `Bearer ${token}`, 'x-mc-api-forward-to-version': 'v2', }, originalUrl: '/foo/bar', @@ -249,13 +254,14 @@ describe('when "X-MC-API-Cloud-Identifier" is missing', () => { }); describe('when "X-MC-API-Forward-To-Version" is missing', () => { it('should throw a validation error', async () => { + const token = await fixtureJWTToken.createToken({ + issuer: CLOUD_IDENTIFIERS.GCP_EU, + audience: 'http://test-server/foo/bar', + }); const fakeRequest = { method: 'GET', headers: { - authorization: `Bearer ${fixtureJWTToken.createToken({ - issuer: CLOUD_IDENTIFIERS.GCP_EU, - audience: 'http://test-server/foo/bar', - })}`, + authorization: `Bearer ${token}`, 'x-mc-api-cloud-identifier': CLOUD_IDENTIFIERS.GCP_EU, }, originalUrl: '/foo/bar', diff --git a/packages/jest-preset-mc-app/module-exports-resolver.js b/packages/jest-preset-mc-app/module-exports-resolver.js index edfa7ec834..8e32d49a5e 100644 --- a/packages/jest-preset-mc-app/module-exports-resolver.js +++ b/packages/jest-preset-mc-app/module-exports-resolver.js @@ -2,6 +2,7 @@ const modulesWithFaultyExports = [ '@react-hook/resize-observer', '@react-hook/passive-layout-effect', '@react-hook/latest', + 'jose', ]; // https://jestjs.io/docs/configuration#resolver-string diff --git a/playground/.env b/playground/.env index 12a40f8493..f2ed1c36ba 100644 --- a/playground/.env +++ b/playground/.env @@ -1,5 +1,5 @@ CLOUD_IDENTIFIER="gcp-eu" -APP_URL="https://app-kit-playground.vercel.app" +APP_URL="https://${VERCEL_URL}" ECHO_SERVER_URL="https://app-kit-playground.vercel.app/api/echo" PLAYGROUND_API_AUDIENCE="https://app-kit-playground.vercel.app" HOST_GCP_STAGING="" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5e95bea3d2..eda1681815 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1245,8 +1245,8 @@ importers: specifier: ^9.0.2 version: 9.0.2 jose: - specifier: 2.0.7 - version: 2.0.7 + specifier: 5.4.0 + version: 5.4.0 msw: specifier: 0.49.3 version: 0.49.3(typescript@5.0.4) @@ -6338,10 +6338,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.23.5 + '@babel/compat-data': 7.22.9 '@babel/core': 7.23.0 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.0) '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.0) dev: false @@ -6352,10 +6352,10 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/compat-data': 7.23.5 + '@babel/compat-data': 7.22.9 '@babel/core': 7.24.5 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-plugin-utils': 7.24.0 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.5) '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.5) dev: true @@ -7544,7 +7544,7 @@ packages: dependencies: '@babel/core': 7.23.0 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.23.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-transform-react-display-name': 7.22.5(@babel/core@7.23.0) '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.0) '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.23.0) @@ -7559,7 +7559,7 @@ packages: dependencies: '@babel/core': 7.24.5 '@babel/helper-plugin-utils': 7.22.5 - '@babel/helper-validator-option': 7.23.5 + '@babel/helper-validator-option': 7.22.15 '@babel/plugin-transform-react-display-name': 7.22.5(@babel/core@7.24.5) '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.5) '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.24.5) @@ -13880,6 +13880,7 @@ packages: /@panva/asn1.js@1.0.0: resolution: {integrity: sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==} engines: {node: '>=10.13.0'} + dev: false /@peculiar/asn1-schema@2.3.6: resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==} @@ -23430,11 +23431,16 @@ packages: engines: {node: '>=10.13.0 < 13 || >=13.7.0'} dependencies: '@panva/asn1.js': 1.0.0 + dev: false /jose@4.13.2: resolution: {integrity: sha512-GMUKtV+l05F6NY/06nM7rucHM6Ktvw6sxnyRqINBNWS/hCM/bBk7kanOEckRP8xtC/jzuGfTRVZvkjjuy+g4dA==} dev: false + /jose@5.4.0: + resolution: {integrity: sha512-6rpxTHPAQyWMb9A35BroFl1Sp0ST3DpPcm5EVIxZxdH+e0Hv9fwhyB3XLKFUcHNpdSDnETmBfuPPTTlYz5+USw==} + dev: true + /js-levenshtein@1.1.6: resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==} engines: {node: '>=0.10.0'}