Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(deps): update dependency jose to v5 - abandoned #3540

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
6 changes: 6 additions & 0 deletions jest.test.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
2 changes: 1 addition & 1 deletion packages-backend/express/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
60 changes: 45 additions & 15 deletions packages-backend/express/src/middlewares/fixtures/jwt-token.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,49 @@
import { JWT, JWK, JWKS } from 'jose';
import {
exportJWK,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to rebuild the fixtures because most of the jose APIs have changed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

jose APIs are now async so I found this initializer the simplest way to go.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also check how we initialize this in our MC services (auth package).

Anyway, if it does the job all good.

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 };
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ function waitForSessionMiddleware(
afterEach(() => {
mockServer.resetHandlers();
});
beforeAll(() =>
beforeAll(async () => {
await fixtureJWTToken.initialize();
mockServer.listen({
onUnhandledRequest: 'error',
})
);
});
});
afterAll(() => mockServer.close());

describe.each`
Expand All @@ -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<string, unknown>;
requestOptions?: Record<string, unknown>;
}) {
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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}${
Expand Down Expand Up @@ -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',
Expand All @@ -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 "/"
},
Expand All @@ -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,
Expand Down Expand Up @@ -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',
Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions packages/jest-preset-mc-app/module-exports-resolver.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ const modulesWithFaultyExports = [
'@react-hook/resize-observer',
'@react-hook/passive-layout-effect',
'@react-hook/latest',
'jose',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed as jose now only exports esm module.

];

// https://jestjs.io/docs/configuration#resolver-string
Expand Down
2 changes: 1 addition & 1 deletion playground/.env
Original file line number Diff line number Diff line change
@@ -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=""
Expand Down
26 changes: 16 additions & 10 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading