Skip to content

Commit 5fe27e5

Browse files
authored
fix: switch from jsonwebtoken to jose for jwt signing/verification (#678)
- Supports async jwt signing/verification using libuv - Add support for OKP (ed25519/Ed448) jwks - Cleaner implementation across jwt code
1 parent ed993b5 commit 5fe27e5

21 files changed

+12152
-10035
lines changed

babel.config.cjs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = {
2+
presets: [['@babel/preset-env', { targets: { node: 'current' } }]],
3+
};

jest.config.js renamed to jest.config.cjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
module.exports = {
22
preset: 'ts-jest',
3-
testSequencer: './jest.sequencer.js',
3+
testSequencer: './jest.sequencer.cjs',
44
transform: {
5+
'^.+/node_modules/jose/.+\\.[jt]s$': 'babel-jest',
6+
'^.+\\.mjs$': 'babel-jest',
57
'^.+\\.(t|j)sx?$': 'ts-jest',
68
},
9+
transformIgnorePatterns: ['node_modules/(?!(jose)/)'],
710
moduleNameMapper: {
811
'^@storage/(.*)$': '<rootDir>/src/storage/$1',
912
'^@internal/(.*)$': '<rootDir>/src/internal/$1',
File renamed without changes.

package-lock.json

+11,951-9,842
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@
6767
"glob": "^11.0.0",
6868
"ioredis": "^5.2.4",
6969
"ip-address": "^10.0.1",
70-
"jsonwebtoken": "^9.0.2",
70+
"jose": "^6.0.10",
7171
"knex": "^3.1.0",
7272
"lru-cache": "^10.2.0",
7373
"md5-file": "^5.0.0",
@@ -84,14 +84,15 @@
8484
},
8585
"devDependencies": {
8686
"@aws-sdk/s3-presigned-post": "3.654.0",
87+
"@babel/preset-env": "^7.26.9",
88+
"@babel/preset-typescript": "^7.27.0",
8789
"@types/async-retry": "^1.4.5",
8890
"@types/busboy": "^1.3.0",
8991
"@types/crypto-js": "^4.1.1",
9092
"@types/fs-extra": "^9.0.13",
9193
"@types/glob": "^8.1.0",
9294
"@types/jest": "^29.2.1",
9395
"@types/js-yaml": "^4.0.5",
94-
"@types/jsonwebtoken": "^9.0.5",
9596
"@types/multistream": "^4.1.3",
9697
"@types/mustache": "^4.2.2",
9798
"@types/node": "^20.11.5",
@@ -100,21 +101,21 @@
100101
"@types/xml2js": "^0.4.14",
101102
"@typescript-eslint/eslint-plugin": "^8.7.0",
102103
"@typescript-eslint/parser": "^8.7.0",
103-
"babel-jest": "^29.2.2",
104+
"babel-jest": "^29.7.0",
104105
"esbuild": "0.21.5",
105106
"eslint": "^8.9.0",
106107
"eslint-config-prettier": "^8.10.0",
107108
"eslint-plugin-prettier": "^4.2.1",
108109
"form-data": "^4.0.0",
109-
"jest": "^29.2.2",
110+
"jest": "^29.7.0",
110111
"js-yaml": "^4.1.0",
111112
"json-schema-to-ts": "^3.0.0",
112113
"mustache": "^4.2.0",
113114
"pino-pretty": "^8.1.0",
114115
"prettier": "^2.8.8",
115116
"resolve-tspaths": "^0.8.19",
116117
"stream-buffers": "^3.0.2",
117-
"ts-jest": "^29.0.3",
118+
"ts-jest": "^29.3.2",
118119
"ts-node-dev": "^1.1.8",
119120
"tsx": "^4.16.0",
120121
"tus-js-client": "^3.1.0",

src/config.ts

+21-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dotenv from 'dotenv'
2-
import jwt from 'jsonwebtoken'
32
import type { DBMigration } from '@internal/database/migrations'
3+
import { SignJWT } from 'jose'
44

55
export type StorageBackendType = 'file' | 's3'
66
export enum MultitenantMigrationStrategy {
@@ -93,8 +93,8 @@ type StorageConfigType = {
9393
requestTraceHeader?: string
9494
requestEtagHeaders: string[]
9595
responseSMaxAge: number
96-
anonKey: string
97-
serviceKey: string
96+
anonKeyAsync: Promise<string>
97+
serviceKeyAsync: Promise<string>
9898
storageBackendType: StorageBackendType
9999
tenantId: string
100100
requestUrlLengthLimit: number
@@ -259,10 +259,6 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
259259
'REQUEST_ADMIN_TRACE_HEADER'
260260
),
261261

262-
// Auth
263-
serviceKey: getOptionalConfigFromEnv('SERVICE_KEY') || '',
264-
anonKey: getOptionalConfigFromEnv('ANON_KEY') || '',
265-
266262
encryptionKey: getOptionalConfigFromEnv('AUTH_ENCRYPTION_KEY', 'ENCRYPTION_KEY') || '',
267263
jwtSecret: getOptionalIfMultitenantConfigFromEnv('AUTH_JWT_SECRET', 'PGRST_JWT_SECRET') || '',
268264
jwtAlgorithm: getOptionalConfigFromEnv('AUTH_JWT_ALGORITHM', 'PGRST_JWT_ALGORITHM') || 'HS256',
@@ -484,18 +480,26 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
484480
),
485481
} as StorageConfigType
486482

487-
if (!config.isMultitenant && !config.serviceKey) {
488-
config.serviceKey = jwt.sign({ role: config.dbServiceRole }, config.jwtSecret, {
489-
expiresIn: '10y',
490-
algorithm: config.jwtAlgorithm as jwt.Algorithm,
491-
})
483+
const serviceKey = getOptionalConfigFromEnv('SERVICE_KEY') || ''
484+
if (!config.isMultitenant && !serviceKey) {
485+
config.serviceKeyAsync = new SignJWT({ role: config.dbServiceRole })
486+
.setIssuedAt()
487+
.setExpirationTime('10y')
488+
.setProtectedHeader({ alg: 'HS256' })
489+
.sign(new TextEncoder().encode(config.jwtSecret))
490+
} else {
491+
config.serviceKeyAsync = Promise.resolve(serviceKey)
492492
}
493493

494-
if (!config.isMultitenant && !config.anonKey) {
495-
config.anonKey = jwt.sign({ role: config.dbAnonRole }, config.jwtSecret, {
496-
expiresIn: '10y',
497-
algorithm: config.jwtAlgorithm as jwt.Algorithm,
498-
})
494+
const anonKey = getOptionalConfigFromEnv('ANON_KEY') || ''
495+
if (!config.isMultitenant && !anonKey) {
496+
config.anonKeyAsync = new SignJWT({ role: config.dbAnonRole })
497+
.setIssuedAt()
498+
.setExpirationTime('10y')
499+
.setProtectedHeader({ alg: 'HS256' })
500+
.sign(new TextEncoder().encode(config.jwtSecret))
501+
} else {
502+
config.anonKeyAsync = Promise.resolve(anonKey)
499503
}
500504

501505
const jwtJWKS = getOptionalConfigFromEnv('JWT_JWKS') || null

src/http/plugins/jwt.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import fastifyPlugin from 'fastify-plugin'
2-
import { JwtPayload } from 'jsonwebtoken'
2+
import { JWTPayload } from 'jose'
33

44
import { verifyJWT } from '@internal/auth'
55
import { getJwtSecret } from '@internal/database'
@@ -9,7 +9,7 @@ declare module 'fastify' {
99
interface FastifyRequest {
1010
isAuthenticated: boolean
1111
jwt: string
12-
jwtPayload?: JwtPayload & { role?: string }
12+
jwtPayload?: JWTPayload & { role?: string }
1313
owner?: string
1414
}
1515

src/http/plugins/signature-v4.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { getConfig } from '../../config'
99
import { MultipartFile, MultipartValue } from '@fastify/multipart'
1010

1111
const {
12-
anonKey,
13-
serviceKey,
12+
anonKeyAsync,
13+
serviceKeyAsync,
1414
storageS3Region,
1515
isMultitenant,
1616
requestAllowXForwardedPrefix,
@@ -138,7 +138,9 @@ async function createServerSignature(tenantId: string, clientSignature: ClientSi
138138
const awsService = 's3'
139139

140140
if (clientSignature?.sessionToken) {
141-
const tenantAnonKey = isMultitenant ? (await getTenantConfig(tenantId)).anonKey : anonKey
141+
const tenantAnonKey = isMultitenant
142+
? (await getTenantConfig(tenantId)).anonKey
143+
: await anonKeyAsync
142144

143145
if (!tenantAnonKey) {
144146
throw ERRORS.AccessDenied('Missing tenant anon key')
@@ -198,5 +200,5 @@ async function createServerSignature(tenantId: string, clientSignature: ClientSi
198200
},
199201
})
200202

201-
return { signature, claims: undefined, token: serviceKey }
203+
return { signature, claims: undefined, token: await serviceKeyAsync }
202204
}

src/http/routes/admin/jwks.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,7 @@ function validateAddJwkRequest({ jwk, kind }: JwksAddRequestInterface['Body']):
9090
if (jwk.d) {
9191
return { message: 'Invalid asymmetric public jwk. Private fields are not allowed' }
9292
}
93-
// jsonwebtoken does not support OKP (ed25519/Ed448) keys yet, if/when this changes replace this with a break and we should be good to go
94-
return { message: 'OKP jwks are not yet supported. Please use RSA or EC' }
93+
break
9594
default:
9695
return { message: 'Unsupported jwk algorithm ' + jwk.kty }
9796
}

0 commit comments

Comments
 (0)