From 1e1f1f65e410eec4024443ec2fecbd70267a4b35 Mon Sep 17 00:00:00 2001 From: Anders Semb Hermansen Date: Sat, 14 Sep 2024 16:16:56 +0200 Subject: [PATCH] perf: replace jsonwebtoken with jose The jose package has 0 dependencies and is tree shakable ESM. So we get lower actual bundle size and get rid of 10 dependencies. --- packages/payload/package.json | 3 +- packages/payload/src/auth/jwt.ts | 21 ++++ packages/payload/src/auth/operations/login.ts | 11 +- packages/payload/src/auth/operations/me.ts | 4 +- .../payload/src/auth/operations/refresh.ts | 10 +- .../src/auth/operations/resetPassword.ts | 8 +- packages/payload/src/auth/strategies/jwt.ts | 6 +- pnpm-lock.yaml | 104 +++--------------- 8 files changed, 59 insertions(+), 108 deletions(-) create mode 100644 packages/payload/src/auth/jwt.ts diff --git a/packages/payload/package.json b/packages/payload/package.json index ac89b50222c..5d2d1efc03b 100644 --- a/packages/payload/package.json +++ b/packages/payload/package.json @@ -99,8 +99,8 @@ "get-tsconfig": "^4.7.2", "http-status": "1.6.2", "image-size": "^1.1.1", + "jose": "5.9.2", "json-schema-to-typescript": "15.0.1", - "jsonwebtoken": "9.0.2", "minimist": "1.2.8", "pino": "9.3.1", "pino-pretty": "11.2.1", @@ -115,7 +115,6 @@ "@hyrious/esbuild-plugin-commonjs": "^0.2.4", "@payloadcms/eslint-config": "workspace:*", "@types/json-schema": "7.0.15", - "@types/jsonwebtoken": "8.5.9", "@types/minimist": "1.2.2", "@types/nodemailer": "6.4.14", "@types/pluralize": "0.0.33", diff --git a/packages/payload/src/auth/jwt.ts b/packages/payload/src/auth/jwt.ts new file mode 100644 index 00000000000..e054849c429 --- /dev/null +++ b/packages/payload/src/auth/jwt.ts @@ -0,0 +1,21 @@ +import { SignJWT } from 'jose' + +export const jwtSign = async ({ + fieldsToSign, + secret, + tokenExpiration, +}: { + fieldsToSign: Record + secret: string + tokenExpiration: number +}) => { + const secretKey = new TextEncoder().encode(secret) + const issuedAt = Math.floor(Date.now() / 1000) + const exp = issuedAt + tokenExpiration + const token = await new SignJWT(fieldsToSign) + .setProtectedHeader({ alg: 'HS256', typ: 'JWT' }) + .setIssuedAt(issuedAt) + .setExpirationTime(exp) + .sign(secretKey) + return { exp, token } +} diff --git a/packages/payload/src/auth/operations/login.ts b/packages/payload/src/auth/operations/login.ts index cc4535a8487..602787cdd0a 100644 --- a/packages/payload/src/auth/operations/login.ts +++ b/packages/payload/src/auth/operations/login.ts @@ -1,5 +1,3 @@ -import jwt from 'jsonwebtoken' - import type { AuthOperationsFromCollectionSlug, Collection, @@ -16,6 +14,7 @@ import { killTransaction } from '../../utilities/killTransaction.js' import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js' import { getFieldsToSign } from '../getFieldsToSign.js' import isLocked from '../isLocked.js' +import { jwtSign } from '../jwt.js' import { authenticateLocalStrategy } from '../strategies/local/authenticate.js' import { incrementLoginAttempts } from '../strategies/local/incrementLoginAttempts.js' import { resetLoginAttempts } from '../strategies/local/resetLoginAttempts.js' @@ -232,8 +231,10 @@ export const loginOperation = async ( })) || user }, Promise.resolve()) - const token = jwt.sign(fieldsToSign, secret, { - expiresIn: collectionConfig.auth.tokenExpiration, + const { exp, token } = await jwtSign({ + fieldsToSign, + secret, + tokenExpiration: collectionConfig.auth.tokenExpiration, }) req.user = user @@ -306,7 +307,7 @@ export const loginOperation = async ( }, Promise.resolve()) let result: { user: DataFromCollectionSlug } & Result = { - exp: (jwt.decode(token) as jwt.JwtPayload).exp, + exp, token, user, } diff --git a/packages/payload/src/auth/operations/me.ts b/packages/payload/src/auth/operations/me.ts index 1774277a0f8..a94885fd20d 100644 --- a/packages/payload/src/auth/operations/me.ts +++ b/packages/payload/src/auth/operations/me.ts @@ -1,4 +1,4 @@ -import jwt from 'jsonwebtoken' +import { decodeJwt } from 'jose' import type { Collection } from '../../collections/config/types.js' import type { PayloadRequest } from '../../types/index.js' @@ -68,7 +68,7 @@ export const meOperation = async (args: Arguments): Promise = result.user = user if (currentToken) { - const decoded = jwt.decode(currentToken) as jwt.JwtPayload + const decoded = decodeJwt(currentToken) if (decoded) { result.exp = decoded.exp } diff --git a/packages/payload/src/auth/operations/refresh.ts b/packages/payload/src/auth/operations/refresh.ts index d0dbb7e31a2..14b6026a389 100644 --- a/packages/payload/src/auth/operations/refresh.ts +++ b/packages/payload/src/auth/operations/refresh.ts @@ -1,4 +1,3 @@ -import jwt from 'jsonwebtoken' import url from 'url' import type { BeforeOperationHook, Collection } from '../../collections/config/types.js' @@ -10,6 +9,7 @@ import { commitTransaction } from '../../utilities/commitTransaction.js' import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { getFieldsToSign } from '../getFieldsToSign.js' +import { jwtSign } from '../jwt.js' export type Result = { exp: number @@ -98,12 +98,12 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise user: args?.req?.user, }) - const refreshedToken = jwt.sign(fieldsToSign, secret, { - expiresIn: collectionConfig.auth.tokenExpiration, + const { exp, token: refreshedToken } = await jwtSign({ + fieldsToSign, + secret, + tokenExpiration: collectionConfig.auth.tokenExpiration, }) - const exp = (jwt.decode(refreshedToken) as Record).exp as number - result = { exp, refreshedToken, diff --git a/packages/payload/src/auth/operations/resetPassword.ts b/packages/payload/src/auth/operations/resetPassword.ts index ac430e42a07..654b143a677 100644 --- a/packages/payload/src/auth/operations/resetPassword.ts +++ b/packages/payload/src/auth/operations/resetPassword.ts @@ -1,5 +1,4 @@ import httpStatus from 'http-status' -import jwt from 'jsonwebtoken' import type { Collection } from '../../collections/config/types.js' import type { PayloadRequest } from '../../types/index.js' @@ -9,6 +8,7 @@ import { commitTransaction } from '../../utilities/commitTransaction.js' import { initTransaction } from '../../utilities/initTransaction.js' import { killTransaction } from '../../utilities/killTransaction.js' import { getFieldsToSign } from '../getFieldsToSign.js' +import { jwtSign } from '../jwt.js' import { authenticateLocalStrategy } from '../strategies/local/authenticate.js' import { generatePasswordSaltHash } from '../strategies/local/generatePasswordSaltHash.js' @@ -99,8 +99,10 @@ export const resetPasswordOperation = async (args: Arguments): Promise = user, }) - const token = jwt.sign(fieldsToSign, secret, { - expiresIn: collectionConfig.auth.tokenExpiration, + const { token } = await jwtSign({ + fieldsToSign, + secret, + tokenExpiration: collectionConfig.auth.tokenExpiration, }) const fullUser = await payload.findByID({ diff --git a/packages/payload/src/auth/strategies/jwt.ts b/packages/payload/src/auth/strategies/jwt.ts index c5ce2e3ba8e..00f4cbc10de 100644 --- a/packages/payload/src/auth/strategies/jwt.ts +++ b/packages/payload/src/auth/strategies/jwt.ts @@ -1,4 +1,4 @@ -import jwt from 'jsonwebtoken' +import { jwtVerify } from 'jose' import type { Payload, Where } from '../../types/index.js' import type { AuthStrategyFunction, User } from '../index.js' @@ -81,8 +81,8 @@ export const JWTAuthentication: AuthStrategyFunction = async ({ return { user: null } } - const decodedPayload = jwt.verify(token, payload.secret) as jwt.JwtPayload & JWTToken - + const secretKey = new TextEncoder().encode(payload.secret) + const { payload: decodedPayload } = await jwtVerify(token, secretKey) const collection = payload.collections[decodedPayload.collection] const user = await payload.findByID({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0270a1ceb0a..a53f094dd15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -839,12 +839,12 @@ importers: image-size: specifier: ^1.1.1 version: 1.1.1 + jose: + specifier: 5.9.2 + version: 5.9.2 json-schema-to-typescript: specifier: 15.0.1 version: 15.0.1 - jsonwebtoken: - specifier: 9.0.2 - version: 9.0.2 minimist: specifier: 1.2.8 version: 1.2.8 @@ -882,9 +882,6 @@ importers: '@types/json-schema': specifier: 7.0.15 version: 7.0.15 - '@types/jsonwebtoken': - specifier: 8.5.9 - version: 8.5.9 '@types/minimist': specifier: 1.2.2 version: 1.2.2 @@ -957,7 +954,7 @@ importers: version: link:../payload ts-jest: specifier: ^29.1.0 - version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0))(typescript@5.6.2) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.19.12)(jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0))(typescript@5.6.2) packages/plugin-cloud-storage: dependencies: @@ -1154,7 +1151,7 @@ importers: version: link:../payload ts-jest: specifier: ^29.1.0 - version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0))(typescript@5.6.2) + version: 29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.19.12)(jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0))(typescript@5.6.2) packages/plugin-seo: dependencies: @@ -1467,7 +1464,7 @@ importers: version: link:../plugin-cloud-storage uploadthing: specifier: ^6.10.1 - version: 6.13.2(express@4.19.2)(next@15.0.0-canary.104(@babel/core@7.25.2)(@playwright/test@1.46.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-06d0b89e-20240801(react@19.0.0-rc-06d0b89e-20240801))(react@19.0.0-rc-06d0b89e-20240801)(sass@1.77.4)) + version: 6.13.2(express@4.19.2)(next@15.0.0-canary.104(@babel/core@7.25.2)(@playwright/test@1.46.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@0.0.0-experimental-48eb8f4-20240822)(react-dom@19.0.0-rc-06d0b89e-20240801(react@19.0.0-rc-06d0b89e-20240801))(react@19.0.0-rc-06d0b89e-20240801)(sass@1.77.4)) devDependencies: payload: specifier: workspace:* @@ -1828,7 +1825,7 @@ importers: version: 5.6.2 uploadthing: specifier: ^6.10.1 - version: 6.13.2(express@4.19.2)(next@15.0.0-canary.104(@babel/core@7.25.2)(@playwright/test@1.46.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-06d0b89e-20240801(react@19.0.0-rc-06d0b89e-20240801))(react@19.0.0-rc-06d0b89e-20240801)(sass@1.77.4)) + version: 6.13.2(express@4.19.2)(next@15.0.0-canary.104(@babel/core@7.25.2)(@playwright/test@1.46.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@0.0.0-experimental-48eb8f4-20240822)(react-dom@19.0.0-rc-06d0b89e-20240801(react@19.0.0-rc-06d0b89e-20240801))(react@19.0.0-rc-06d0b89e-20240801)(sass@1.77.4)) uuid: specifier: 10.0.0 version: 10.0.0 @@ -4425,9 +4422,6 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} - '@types/jsonwebtoken@8.5.9': - resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} - '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} @@ -7016,6 +7010,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + jose@5.9.2: + resolution: {integrity: sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -7105,23 +7102,13 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} - jsonwebtoken@9.0.2: - resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} - engines: {node: '>=12', npm: '>=6'} - jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} - jwa@1.4.1: - resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} - jwa@2.0.0: resolution: {integrity: sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==} - jws@3.2.2: - resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} - jws@4.0.0: resolution: {integrity: sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==} @@ -7172,6 +7159,7 @@ packages: libsql@0.3.19: resolution: {integrity: sha512-Aj5cQ5uk/6fHdmeW0TiXK42FqUlwx7ytmMLPSaUQPin5HKKKuUPD62MAbN4OEweGBBI7q1BekoEN4gPUEL6MZA==} + cpu: [x64, arm64, wasm32] os: [darwin, linux, win32] lie@3.1.1: @@ -7217,33 +7205,12 @@ packages: lodash.get@4.4.2: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - lodash.includes@4.3.0: - resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} - - lodash.isboolean@3.0.3: - resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} - - lodash.isinteger@4.0.4: - resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} - - lodash.isnumber@3.0.3: - resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} - - lodash.isplainobject@4.0.6: - resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - - lodash.isstring@4.0.1: - resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.once@4.1.1: - resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} - lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -13059,10 +13026,6 @@ snapshots: dependencies: '@types/node': 22.5.4 - '@types/jsonwebtoken@8.5.9': - dependencies: - '@types/node': 22.5.4 - '@types/keyv@3.1.4': dependencies: '@types/node': 22.5.4 @@ -16247,6 +16210,8 @@ snapshots: jiti@1.21.6: {} + jose@5.9.2: {} + joycon@3.1.1: {} js-base64@3.7.7: {} @@ -16350,19 +16315,6 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 - jsonwebtoken@9.0.2: - dependencies: - jws: 3.2.2 - lodash.includes: 4.3.0 - lodash.isboolean: 3.0.3 - lodash.isinteger: 4.0.4 - lodash.isnumber: 3.0.3 - lodash.isplainobject: 4.0.6 - lodash.isstring: 4.0.1 - lodash.once: 4.1.1 - ms: 2.1.3 - semver: 7.6.3 - jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -16370,23 +16322,12 @@ snapshots: object.assign: 4.1.5 object.values: 1.2.0 - jwa@1.4.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - jwa@2.0.0: dependencies: buffer-equal-constant-time: 1.0.1 ecdsa-sig-formatter: 1.0.11 safe-buffer: 5.2.1 - jws@3.2.2: - dependencies: - jwa: 1.4.1 - safe-buffer: 5.2.1 - jws@4.0.0: dependencies: jwa: 2.0.0 @@ -16494,24 +16435,10 @@ snapshots: lodash.get@4.4.2: {} - lodash.includes@4.3.0: {} - - lodash.isboolean@3.0.3: {} - - lodash.isinteger@4.0.4: {} - - lodash.isnumber@3.0.3: {} - - lodash.isplainobject@4.0.6: {} - - lodash.isstring@4.0.1: {} - lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} - lodash.once@4.1.1: {} - lodash@4.17.21: {} log-update@6.1.0: @@ -18403,7 +18330,7 @@ snapshots: optionalDependencies: typescript: 5.6.2 - ts-jest@29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0))(typescript@5.6.2): + ts-jest@29.2.4(@babel/core@7.25.2)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.25.2))(esbuild@0.19.12)(jest@29.7.0(@types/node@22.5.4)(babel-plugin-macros@3.1.0))(typescript@5.6.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -18421,6 +18348,7 @@ snapshots: '@jest/transform': 29.7.0 '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.25.2) + esbuild: 0.19.12 ts-pattern@5.3.1: {} @@ -18588,7 +18516,7 @@ snapshots: escalade: 3.1.2 picocolors: 1.0.1 - uploadthing@6.13.2(express@4.19.2)(next@15.0.0-canary.104(@babel/core@7.25.2)(@playwright/test@1.46.0)(babel-plugin-macros@3.1.0)(react-dom@19.0.0-rc-06d0b89e-20240801(react@19.0.0-rc-06d0b89e-20240801))(react@19.0.0-rc-06d0b89e-20240801)(sass@1.77.4)): + uploadthing@6.13.2(express@4.19.2)(next@15.0.0-canary.104(@babel/core@7.25.2)(@playwright/test@1.46.0)(babel-plugin-macros@3.1.0)(babel-plugin-react-compiler@0.0.0-experimental-48eb8f4-20240822)(react-dom@19.0.0-rc-06d0b89e-20240801(react@19.0.0-rc-06d0b89e-20240801))(react@19.0.0-rc-06d0b89e-20240801)(sass@1.77.4)): dependencies: '@effect/schema': 0.68.12(effect@3.4.5) '@uploadthing/mime-types': 0.2.10