Skip to content

Commit d4fea13

Browse files
itslennyfenos
authored andcommitted
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 d4fea13

36 files changed

+17199
-11575
lines changed

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Base stage for shared environment setup
2-
FROM node:20-alpine3.20 as base
2+
FROM node:22-alpine3.21 as base
33
RUN apk add --no-cache g++ make python3
44
WORKDIR /app
55
COPY package.json package-lock.json ./

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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE tenants ADD COLUMN IF NOT EXISTS database_pool_mode TEXT NULL;

package-lock.json

+16,476-11,243
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+7-5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"node": ">= 14.0.0"
2929
},
3030
"dependencies": {
31+
"@aws-sdk/client-ecs": "^3.795.0",
3132
"@aws-sdk/client-s3": "3.654.0",
3233
"@aws-sdk/lib-storage": "3.654.0",
3334
"@aws-sdk/s3-request-presigner": "3.654.0",
@@ -67,7 +68,7 @@
6768
"glob": "^11.0.0",
6869
"ioredis": "^5.2.4",
6970
"ip-address": "^10.0.1",
70-
"jsonwebtoken": "^9.0.2",
71+
"jose": "^6.0.10",
7172
"knex": "^3.1.0",
7273
"lru-cache": "^10.2.0",
7374
"md5-file": "^5.0.0",
@@ -84,14 +85,15 @@
8485
},
8586
"devDependencies": {
8687
"@aws-sdk/s3-presigned-post": "3.654.0",
88+
"@babel/preset-env": "^7.26.9",
89+
"@babel/preset-typescript": "^7.27.0",
8790
"@types/async-retry": "^1.4.5",
8891
"@types/busboy": "^1.3.0",
8992
"@types/crypto-js": "^4.1.1",
9093
"@types/fs-extra": "^9.0.13",
9194
"@types/glob": "^8.1.0",
9295
"@types/jest": "^29.2.1",
9396
"@types/js-yaml": "^4.0.5",
94-
"@types/jsonwebtoken": "^9.0.5",
9597
"@types/multistream": "^4.1.3",
9698
"@types/mustache": "^4.2.2",
9799
"@types/node": "^20.11.5",
@@ -100,21 +102,21 @@
100102
"@types/xml2js": "^0.4.14",
101103
"@typescript-eslint/eslint-plugin": "^8.7.0",
102104
"@typescript-eslint/parser": "^8.7.0",
103-
"babel-jest": "^29.2.2",
105+
"babel-jest": "^29.7.0",
104106
"esbuild": "0.21.5",
105107
"eslint": "^8.9.0",
106108
"eslint-config-prettier": "^8.10.0",
107109
"eslint-plugin-prettier": "^4.2.1",
108110
"form-data": "^4.0.0",
109-
"jest": "^29.2.2",
111+
"jest": "^29.7.0",
110112
"js-yaml": "^4.1.0",
111113
"json-schema-to-ts": "^3.0.0",
112114
"mustache": "^4.2.0",
113115
"pino-pretty": "^8.1.0",
114116
"prettier": "^2.8.8",
115117
"resolve-tspaths": "^0.8.19",
116118
"stream-buffers": "^3.0.2",
117-
"ts-jest": "^29.0.3",
119+
"ts-jest": "^29.3.2",
118120
"ts-node-dev": "^1.1.8",
119121
"tsx": "^4.16.0",
120122
"tus-js-client": "^3.1.0",

src/config.ts

+23-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 {
@@ -86,15 +86,16 @@ type StorageConfigType = {
8686
databaseURL: string
8787
databaseSSLRootCert?: string
8888
databasePoolURL?: string
89+
databasePoolMode?: 'single_use' | 'recycle'
8990
databaseMaxConnections: number
9091
databaseFreePoolAfterInactivity: number
9192
databaseConnectionTimeout: number
9293
region: string
9394
requestTraceHeader?: string
9495
requestEtagHeaders: string[]
9596
responseSMaxAge: number
96-
anonKey: string
97-
serviceKey: string
97+
anonKeyAsync: Promise<string>
98+
serviceKeyAsync: Promise<string>
9899
storageBackendType: StorageBackendType
99100
tenantId: string
100101
requestUrlLengthLimit: number
@@ -259,10 +260,6 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
259260
'REQUEST_ADMIN_TRACE_HEADER'
260261
),
261262

262-
// Auth
263-
serviceKey: getOptionalConfigFromEnv('SERVICE_KEY') || '',
264-
anonKey: getOptionalConfigFromEnv('ANON_KEY') || '',
265-
266263
encryptionKey: getOptionalConfigFromEnv('AUTH_ENCRYPTION_KEY', 'ENCRYPTION_KEY') || '',
267264
jwtSecret: getOptionalIfMultitenantConfigFromEnv('AUTH_JWT_SECRET', 'PGRST_JWT_SECRET') || '',
268265
jwtAlgorithm: getOptionalConfigFromEnv('AUTH_JWT_ALGORITHM', 'PGRST_JWT_ALGORITHM') || 'HS256',
@@ -357,6 +354,7 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
357354
databaseSSLRootCert: getOptionalConfigFromEnv('DATABASE_SSL_ROOT_CERT'),
358355
databaseURL: getOptionalIfMultitenantConfigFromEnv('DATABASE_URL') || '',
359356
databasePoolURL: getOptionalConfigFromEnv('DATABASE_POOL_URL') || '',
357+
databasePoolMode: getOptionalConfigFromEnv('DATABASE_POOL_MODE'),
360358
databaseMaxConnections: parseInt(
361359
getOptionalConfigFromEnv('DATABASE_MAX_CONNECTIONS') || '20',
362360
10
@@ -484,18 +482,26 @@ export function getConfig(options?: { reload?: boolean }): StorageConfigType {
484482
),
485483
} as StorageConfigType
486484

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-
})
485+
const serviceKey = getOptionalConfigFromEnv('SERVICE_KEY') || ''
486+
if (!config.isMultitenant && !serviceKey) {
487+
config.serviceKeyAsync = new SignJWT({ role: config.dbServiceRole })
488+
.setIssuedAt()
489+
.setExpirationTime('10y')
490+
.setProtectedHeader({ alg: 'HS256' })
491+
.sign(new TextEncoder().encode(config.jwtSecret))
492+
} else {
493+
config.serviceKeyAsync = Promise.resolve(serviceKey)
492494
}
493495

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-
})
496+
const anonKey = getOptionalConfigFromEnv('ANON_KEY') || ''
497+
if (!config.isMultitenant && !anonKey) {
498+
config.anonKeyAsync = new SignJWT({ role: config.dbAnonRole })
499+
.setIssuedAt()
500+
.setExpirationTime('10y')
501+
.setProtectedHeader({ alg: 'HS256' })
502+
.sign(new TextEncoder().encode(config.jwtSecret))
503+
} else {
504+
config.anonKeyAsync = Promise.resolve(anonKey)
499505
}
500506

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

src/http/plugins/jwt.ts

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

4-
import { verifyJWT } from '@internal/auth'
4+
import { verifyCacheJwt, verifyJWT } from '@internal/auth'
55
import { getJwtSecret } from '@internal/database'
66
import { ERRORS } from '@internal/errors'
77

88
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

@@ -37,7 +37,7 @@ export const jwt = fastifyPlugin(
3737
const { secret, jwks } = await getJwtSecret(request.tenantId)
3838

3939
try {
40-
const payload = await verifyJWT(request.jwt, secret, jwks || null)
40+
const payload = await verifyCacheJwt(request.jwt, secret, jwks || null)
4141
request.jwtPayload = payload
4242
request.owner = payload.sub
4343
request.isAuthenticated = true

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
}

src/http/routes/admin/tenants.ts

+9
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const patchSchema = {
2626
anonKey: { type: 'string' },
2727
databaseUrl: { type: 'string' },
2828
databasePoolUrl: { type: 'string', nullable: true },
29+
databasePoolMode: { type: 'string', nullable: true },
2930
maxConnections: { type: 'number' },
3031
jwks: { type: 'object', nullable: true },
3132
fileSizeLimit: { type: 'number' },
@@ -112,6 +113,7 @@ export default async function routes(fastify: FastifyInstance) {
112113
anon_key,
113114
database_url,
114115
database_pool_url,
116+
database_pool_mode,
115117
max_connections,
116118
file_size_limit,
117119
jwt_secret,
@@ -130,6 +132,7 @@ export default async function routes(fastify: FastifyInstance) {
130132
anonKey: decrypt(anon_key),
131133
databaseUrl: decrypt(database_url),
132134
databasePoolUrl: database_pool_url ? decrypt(database_pool_url) : undefined,
135+
databasePoolMode: database_pool_mode,
133136
maxConnections: max_connections ? Number(max_connections) : undefined,
134137
fileSizeLimit: Number(file_size_limit),
135138
jwtSecret: decrypt(jwt_secret),
@@ -164,6 +167,7 @@ export default async function routes(fastify: FastifyInstance) {
164167
anon_key,
165168
database_url,
166169
database_pool_url,
170+
database_pool_mode,
167171
max_connections,
168172
file_size_limit,
169173
jwt_secret,
@@ -188,6 +192,7 @@ export default async function routes(fastify: FastifyInstance) {
188192
: database_pool_url
189193
? decrypt(database_pool_url)
190194
: undefined,
195+
databasePoolMode: database_pool_mode,
191196
maxConnections: max_connections ? Number(max_connections) : undefined,
192197
fileSizeLimit: Number(file_size_limit),
193198
jwtSecret: decrypt(jwt_secret),
@@ -217,6 +222,7 @@ export default async function routes(fastify: FastifyInstance) {
217222
const {
218223
anonKey,
219224
databaseUrl,
225+
databasePoolMode,
220226
fileSizeLimit,
221227
jwtSecret,
222228
jwks,
@@ -233,6 +239,7 @@ export default async function routes(fastify: FastifyInstance) {
233239
anon_key: encrypt(anonKey),
234240
database_url: encrypt(databaseUrl),
235241
database_pool_url: databasePoolUrl ? encrypt(databasePoolUrl) : undefined,
242+
database_pool_mode: databasePoolMode,
236243
max_connections: maxConnections ? Number(maxConnections) : undefined,
237244
file_size_limit: fileSizeLimit,
238245
jwt_secret: encrypt(jwtSecret),
@@ -280,6 +287,7 @@ export default async function routes(fastify: FastifyInstance) {
280287
serviceKey,
281288
features,
282289
databasePoolUrl,
290+
databasePoolMode,
283291
maxConnections,
284292
tracingMode,
285293
disableEvents,
@@ -295,6 +303,7 @@ export default async function routes(fastify: FastifyInstance) {
295303
: databasePoolUrl === null
296304
? null
297305
: undefined,
306+
database_pool_mode: databasePoolMode,
298307
max_connections: maxConnections ? Number(maxConnections) : undefined,
299308
file_size_limit: fileSizeLimit,
300309
jwt_secret: jwtSecret !== undefined ? encrypt(jwtSecret) : undefined,

0 commit comments

Comments
 (0)