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
This commit is contained in:
contip
2025-07-29 16:27:45 -04:00
committed by GitHub
parent 08942494e3
commit b1fa76e397
2 changed files with 48 additions and 7 deletions

View File

@@ -1,5 +1,4 @@
import url from 'url'
import { v4 as uuid } from 'uuid'
import type { Collection } from '../../collections/config/types.js'
import type { Document, PayloadRequest } from '../../types/index.js'
@@ -74,11 +73,10 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
const parsedURL = url.parse(args.req.url!)
const isGraphQL = parsedURL.pathname === config.routes.graphQL
const user = await args.req.payload.findByID({
id: args.req.user.id,
collection: args.req.user.collection,
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
req: args.req,
let user = await req.payload.db.findOne<any>({
collection: collectionConfig.slug,
req,
where: { id: { equals: args.req.user.id } },
})
const sid = args.req.user._sid
@@ -88,7 +86,7 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
throw new Forbidden(args.req.t)
}
const existingSession = user.sessions.find(({ id }) => id === sid)
const existingSession = user.sessions.find(({ id }: { id: number }) => id === sid)
const now = new Date()
const tokenExpInMs = collectionConfig.auth.tokenExpiration * 1000
@@ -106,6 +104,13 @@ export const refreshOperation = async (incomingArgs: Arguments): Promise<Result>
})
}
user = await req.payload.findByID({
id: user.id,
collection: collectionConfig.slug,
depth: isGraphQL ? 0 : args.collection.config.auth.depth,
req: args.req,
})
if (user) {
user.collection = args.req.user.collection
user._strategy = args.req.user._strategy

View File

@@ -262,6 +262,42 @@ describe('Auth', () => {
expect(data.user.custom).toBe('Goodbye, world!')
})
it('keeps apiKey encrypted in DB after refresh operation', async () => {
const apiKey = '987e6543-e21b-12d3-a456-426614174999'
const user = await payload.create({
collection: slug,
data: { email: 'user@example.com', password: 'Password123', apiKey, enableAPIKey: true },
})
const { token } = await payload.login({
collection: 'users',
data: { email: 'user@example.com', password: 'Password123' },
})
await restClient.POST('/users/refresh-token', {
headers: { Authorization: `JWT ${token}` },
})
const raw = await payload.db.findOne<any>({
collection: 'users',
req: { locale: 'en' } as any,
where: { id: { equals: user.id } },
})
expect(raw?.apiKey).not.toContain('-') // still ciphertext
})
it('returns a user with decrypted apiKey after refresh', async () => {
const { token } = await payload.login({
collection: 'users',
data: { email: 'user@example.com', password: 'Password123' },
})
const res = await restClient
.POST('/users/refresh-token', {
headers: { Authorization: `JWT ${token}` },
})
.then((r) => r.json())
expect(res.user.apiKey).toMatch(/[0-9a-f-]{36}/) // UUID string
})
it('should allow a user to be created', async () => {
const response = await restClient.POST(`/${slug}`, {
body: JSON.stringify({