Skip to content

Commit 522f41d

Browse files
authored
chore: bump Node to 24 and replace jsonwebtoken (#1082)
* chore: bump Node to 24 and replace jsonwebtoken * chore: run lint fix * chore: revert nuxt versions * chore: revert in lockfile
1 parent 465a376 commit 522f41d

11 files changed

Lines changed: 221 additions & 295 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
branches: [main]
88

99
env:
10-
NODE_VER: 22.18
10+
NODE_VER: 24.13
1111
CI: true
1212

1313
jobs:

.github/workflows/deploy-docs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
workflow_dispatch:
99

1010
env:
11-
NODE_VER: 22.18
11+
NODE_VER: 24.13
1212
CI: true
1313

1414
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages

.github/workflows/pkg.pr.new.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ on:
88
pull_request:
99

1010
env:
11-
NODE_VER: 22.18
11+
NODE_VER: 24.13
1212

1313
jobs:
1414
build:

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ permissions:
99
id-token: write # required for npm provenance
1010

1111
env:
12-
NODE_VER: 22.18
12+
NODE_VER: 24.13
1313
CI: true
1414

1515
jobs:

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@
6363
"@nuxt/module-builder": "^1.0.2",
6464
"@nuxt/schema": "^3.20.2",
6565
"@nuxtjs/eslint-config-typescript": "^12.1.0",
66-
"@types/node": "^20.19.29",
66+
"@types/node": "^24.10.11",
6767
"eslint": "^9.39.2",
6868
"nuxt": "^3.20.2",
6969
"ofetch": "^1.5.1",

playground-local/nuxt.config.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
export default defineNuxtConfig({
22
compatibilityDate: '2024-04-03',
33
modules: ['../src/module.ts'],
4-
build: {
5-
transpile: ['jsonwebtoken']
6-
},
74
auth: {
85
provider: {
96
type: 'local',

playground-local/package.json

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,13 @@
1212
"test:e2e": "vitest"
1313
},
1414
"dependencies": {
15-
"jsonwebtoken": "^9.0.3",
15+
"jose": "^6.1.3",
1616
"zod": "^3.25.76"
1717
},
1818
"devDependencies": {
1919
"@nuxt/test-utils": "^3.23.0",
20-
"@playwright/test": "^1.57.0",
21-
"@types/jsonwebtoken": "^9.0.10",
22-
"@types/node": "^20.19.29",
20+
"@playwright/test": "^1.58.2",
21+
"@types/node": "^24.10.11",
2322
"@vue/test-utils": "^2.4.6",
2423
"nuxt": "^3.20.2",
2524
"typescript": "^5.8.3",

playground-local/server/api/auth/refresh.post.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createError, eventHandler, getRequestHeader, readBody } from 'h3'
2-
import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, refreshUserAccessToken } from '~/server/utils/session'
2+
import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, refreshUserAccessToken, userSchema } from '~/server/utils/session'
33

44
/*
55
* DISCLAIMER!
@@ -19,16 +19,24 @@ export default eventHandler(async (event) => {
1919
}
2020

2121
// Verify
22-
const decoded = decodeToken(refreshToken)
22+
const decoded = await decodeToken(refreshToken)
2323
if (!decoded) {
2424
throw createError({
2525
statusCode: 401,
2626
message: 'Unauthorized, refreshToken can\'t be verified'
2727
})
2828
}
2929

30+
const user = userSchema.safeParse(decoded)
31+
if (!user.success) {
32+
throw createError({
33+
statusCode: 401,
34+
message: 'Unauthorized, user shape mismatch'
35+
})
36+
}
37+
3038
// Get the helper (only for demo, use a DB in your implementation)
31-
const userTokens = getTokensByUser(decoded.username)
39+
const userTokens = getTokensByUser(user.data.username)
3240
if (!userTokens) {
3341
throw createError({
3442
statusCode: 401,

playground-local/server/api/auth/user.get.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
import { createError, eventHandler, getRequestHeader } from 'h3'
2-
import { checkUserAccessToken, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser } from '~/server/utils/session'
3-
import type { JwtPayload } from '~/server/utils/session'
2+
import { checkUserAccessToken, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, userSchema } from '~/server/utils/session'
3+
import type { User } from '~/server/utils/session'
44

5-
export default eventHandler((event) => {
5+
export default eventHandler(async (event) => {
66
const authorizationHeader = getRequestHeader(event, 'Authorization')
77
if (typeof authorizationHeader === 'undefined') {
88
throw createError({ statusCode: 403, message: 'Need to pass valid Bearer-authorization header to access this endpoint' })
99
}
1010

1111
const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader)
12-
let decoded: JwtPayload
12+
let decoded: User
1313
try {
14-
const decodeTokenResult = decodeToken(requestAccessToken)
14+
const decodeTokenResult = await decodeToken(requestAccessToken)
1515

1616
if (!decodeTokenResult) {
1717
throw new Error('Expected decoded JwtPayload to be non-empty')
1818
}
19-
decoded = decodeTokenResult
19+
const userParseResult = userSchema.safeParse(decodeTokenResult)
20+
if (!userParseResult.success) {
21+
throw new Error('User shape mismatched')
22+
}
23+
decoded = userParseResult.data
2024
}
2125
catch (error) {
2226
console.error({

playground-local/server/utils/session.ts

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,28 +3,26 @@
33
* This is a demo implementation, please create your own handlers
44
*/
55

6-
import { sign, verify } from 'jsonwebtoken'
6+
import { jwtVerify, SignJWT } from 'jose'
7+
import type { JWTPayload } from 'jose'
78
import { z } from 'zod'
89

910
/**
1011
* This is a demo secret.
1112
* Please ensure that your secret is properly protected.
1213
*/
13-
const SECRET = 'dummy'
14+
const SECRET = new TextEncoder().encode('dummy')
1415

1516
/** 30 seconds */
1617
const ACCESS_TOKEN_TTL = 30
1718

18-
export interface User {
19-
username: string
20-
name: string
21-
picture: string
22-
}
23-
24-
export interface JwtPayload extends User {
25-
scope: Array<'test' | 'user'>
26-
exp?: number
27-
}
19+
export const userSchema = z.object({
20+
username: z.string().min(1),
21+
name: z.string(),
22+
picture: z.string().optional(),
23+
scope: z.enum(['test', 'user']).array().optional(),
24+
})
25+
export type User = z.infer<typeof userSchema>
2826

2927
interface TokensByUser {
3028
access: Map<string, string>
@@ -68,15 +66,10 @@ interface UserTokens {
6866
* Demo function for signing user tokens.
6967
* Your implementation may differ.
7068
*/
71-
export function createUserTokens(user: User): Promise<UserTokens> {
72-
const tokenData: JwtPayload = { ...user, scope: ['test', 'user'] }
73-
const accessToken = sign(tokenData, SECRET, {
74-
expiresIn: ACCESS_TOKEN_TTL
75-
})
76-
const refreshToken = sign(tokenData, SECRET, {
77-
// 1 day
78-
expiresIn: 60 * 60 * 24
79-
})
69+
export async function createUserTokens(user: User): Promise<UserTokens> {
70+
const tokenData: JWTPayload = { ...user, scope: ['test', 'user'] }
71+
const accessToken = await createSignedJwt(tokenData, ACCESS_TOKEN_TTL)
72+
const refreshToken = await createSignedJwt(tokenData, /* 1 day */ 60 * 60 * 24)
8073

8174
// Naive implementation - please implement properly yourself!
8275
const userTokens: TokensByUser = tokensByUser.get(user.username) ?? {
@@ -87,18 +80,18 @@ export function createUserTokens(user: User): Promise<UserTokens> {
8780
userTokens.refresh.set(refreshToken, accessToken)
8881
tokensByUser.set(user.username, userTokens)
8982

90-
// Emulate async work
91-
return Promise.resolve({
83+
return {
9284
accessToken,
9385
refreshToken
94-
})
86+
}
9587
}
9688

9789
/**
9890
* Function for getting the data from a JWT
9991
*/
100-
export function decodeToken(token: string): JwtPayload | undefined {
101-
return verify(token, SECRET) as JwtPayload | undefined
92+
export async function decodeToken(token: string): Promise<JWTPayload> {
93+
const verified = await jwtVerify(token, SECRET)
94+
return verified.payload
10295
}
10396

10497
/**
@@ -138,33 +131,29 @@ export function invalidateAccessToken(tokensByUser: TokensByUser, accessToken: s
138131
tokensByUser.access.delete(accessToken)
139132
}
140133

141-
export function refreshUserAccessToken(tokensByUser: TokensByUser, refreshToken: string): Promise<UserTokens | undefined> {
134+
export async function refreshUserAccessToken(tokensByUser: TokensByUser, refreshToken: string): Promise<UserTokens | undefined> {
142135
// Get the access token
143136
const oldAccessToken = tokensByUser.refresh.get(refreshToken)
144137
if (!oldAccessToken) {
145-
// Promises to emulate async work (e.g. of a DB call)
146-
return Promise.resolve(undefined)
138+
return
147139
}
148140

149141
// Invalidate old access token
150142
invalidateAccessToken(tokensByUser, oldAccessToken)
151143

152144
// Get the user data. In a real implementation this is likely a DB call.
153145
// In this demo we simply re-use the existing JWT data
154-
const jwtUser = decodeToken(refreshToken)
146+
const jwtUser = await decodeToken(refreshToken)
155147
if (!jwtUser) {
156-
return Promise.resolve(undefined)
148+
return
157149
}
158150

159-
const user: User = {
160-
username: jwtUser.username,
161-
picture: jwtUser.picture,
162-
name: jwtUser.name
151+
const user = userSchema.safeParse(jwtUser)
152+
if (!user.success) {
153+
return
163154
}
164155

165-
const accessToken = sign({ ...user, scope: ['test', 'user'] }, SECRET, {
166-
expiresIn: 60 * 5 // 5 minutes
167-
})
156+
const accessToken = await createSignedJwt({ ...user.data, scope: ['test', 'user'] }, /* 5 minutes */ 60 * 5)
168157
tokensByUser.refresh.set(refreshToken, accessToken)
169158
tokensByUser.access.set(accessToken, refreshToken)
170159

@@ -179,3 +168,12 @@ export function extractTokenFromAuthorizationHeader(authorizationHeader: string)
179168
? authorizationHeader.slice(7)
180169
: authorizationHeader
181170
}
171+
172+
function createSignedJwt(payload: JWTPayload, ttlInSeconds: number): Promise<string> {
173+
const unixTimestampNow = Math.floor(Date.now() / 1000)
174+
175+
return new SignJWT(payload)
176+
.setProtectedHeader({ alg: 'HS256' })
177+
.setExpirationTime(unixTimestampNow + ttlInSeconds)
178+
.sign(SECRET)
179+
}

0 commit comments

Comments
 (0)