Skip to content

Commit b1fa76e

Browse files
authored
fix: keep apiKey encrypted in refresh operation (#13063) (#13177)
### What? Prevents decrypted apiKey from being saved back to database on the auth refresh operation. ### Why? References issue #13063: refreshing a token for a logged-in user decrypted `apiKey` and wrote it back in plaintext, corrupting the user record. ### How? The user is now fetched with `db.findOne` instead of `findByID`, preserving the encryption of the key when saved back to the database using `db.updateOne`. The user record is then re-fetched using `findByID`, allowing for the decrypted key to be provided in the response. ### Tests * ✅ keeps apiKey encrypted in DB after refresh * ✅ returns user with decrypted apiKey after refresh Fixes #13063
1 parent 0894249 commit b1fa76e

File tree

2 files changed

+48
-7
lines changed

2 files changed

+48
-7
lines changed

packages/payload/src/auth/operations/refresh.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import url from 'url'
2-
import { v4 as uuid } from 'uuid'
32

43
import type { Collection } from '../../collections/config/types.js'
54
import type { Document, PayloadRequest } from '../../types/index.js'
@@ -74,11 +73,10 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
7473
const parsedURL = url.parse(args.req.url!)
7574
const isGraphQL = parsedURL.pathname === config.routes.graphQL
7675

77-
const user = await args.req.payload.findByID({
78-
id: args.req.user.id,
79-
collection: args.req.user.collection,
80-
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
81-
req: args.req,
76+
let user = await req.payload.db.findOne<any>({
77+
collection: collectionConfig.slug,
78+
req,
79+
where: { id: { equals: args.req.user.id } },
8280
})
8381

8482
const sid = args.req.user._sid
@@ -88,7 +86,7 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
8886
throw new Forbidden(args.req.t)
8987
}
9088

91-
const existingSession = user.sessions.find(({ id }) => id === sid)
89+
const existingSession = user.sessions.find(({ id }: { id: number }) => id === sid)
9290

9391
const now = new Date()
9492
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
@@ -106,6 +104,13 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
106104
})
107105
}
108106

107+
user = await req.payload.findByID({
108+
id: user.id,
109+
collection: collectionConfig.slug,
110+
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
111+
req: args.req,
112+
})
113+
109114
if (user) {
110115
user.collection = args.req.user.collection
111116
user._strategy = args.req.user._strategy

test/auth/int.spec.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,42 @@ describe('Auth', () => {
262262
expect(data.user.custom).toBe('Goodbye, world!')
263263
})
264264

265+
it('keeps apiKey encrypted in DB after refresh operation', async () => {
266+
const apiKey = '987e6543-e21b-12d3-a456-426614174999'
267+
const user = await payload.create({
268+
collection: slug,
269+
data: { email: '[email protected]', password: 'Password123', apiKey, enableAPIKey: true },
270+
})
271+
const { token } = await payload.login({
272+
collection: 'users',
273+
data: { email: '[email protected]', password: 'Password123' },
274+
})
275+
await restClient.POST('/users/refresh-token', {
276+
headers: { Authorization: `JWT ${token}` },
277+
})
278+
const raw = await payload.db.findOne<any>({
279+
collection: 'users',
280+
req: { locale: 'en' } as any,
281+
where: { id: { equals: user.id } },
282+
})
283+
expect(raw?.apiKey).not.toContain('-') // still ciphertext
284+
})
285+
286+
it('returns a user with decrypted apiKey after refresh', async () => {
287+
const { token } = await payload.login({
288+
collection: 'users',
289+
data: { email: '[email protected]', password: 'Password123' },
290+
})
291+
292+
const res = await restClient
293+
.POST('/users/refresh-token', {
294+
headers: { Authorization: `JWT ${token}` },
295+
})
296+
.then((r) => r.json())
297+
298+
expect(res.user.apiKey).toMatch(/[0-9a-f-]{36}/) // UUID string
299+
})
300+
265301
it('should allow a user to be created', async () => {
266302
const response = await restClient.POST(`/${slug}`, {
267303
body: JSON.stringify({

0 commit comments

Comments
 (0)