From 9c453210f8afeaa3ddf00a8d468a99a0ef4720f8 Mon Sep 17 00:00:00 2001 From: Dan Ribbens Date: Mon, 7 Jul 2025 21:23:02 -0400 Subject: [PATCH] fix: payload auth api-key algorithm compatibility (#13076) When saving api-keys in prior versions you can have sha1 generated lookup keys. This ensures compatibility with newer sha256 lookups. --- .../payload/src/auth/baseFields/apiKey.ts | 2 +- .../payload/src/auth/strategies/apiKey.ts | 30 +++++++++++++---- test/auth/int.spec.ts | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/packages/payload/src/auth/baseFields/apiKey.ts b/packages/payload/src/auth/baseFields/apiKey.ts index 196f53441b..cefd88eaf6 100644 --- a/packages/payload/src/auth/baseFields/apiKey.ts +++ b/packages/payload/src/auth/baseFields/apiKey.ts @@ -50,7 +50,7 @@ export const apiKeyFields = [ } if (data?.apiKey) { return crypto - .createHmac('sha1', req.payload.secret) + .createHmac('sha256', req.payload.secret) .update(data.apiKey as string) .digest('hex') } diff --git a/packages/payload/src/auth/strategies/apiKey.ts b/packages/payload/src/auth/strategies/apiKey.ts index 86c3123e37..cc00effbf8 100644 --- a/packages/payload/src/auth/strategies/apiKey.ts +++ b/packages/payload/src/auth/strategies/apiKey.ts @@ -12,16 +12,34 @@ export const APIKeyAuthentication = if (authHeader?.startsWith(`${collectionConfig.slug} API-Key `)) { const apiKey = authHeader.replace(`${collectionConfig.slug} API-Key `, '') - const apiKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex') + + // TODO: V4 remove extra algorithm check + // api keys saved prior to v3.46.0 will have sha1 + const sha1APIKeyIndex = crypto.createHmac('sha1', payload.secret).update(apiKey).digest('hex') + const sha256APIKeyIndex = crypto + .createHmac('sha256', payload.secret) + .update(apiKey) + .digest('hex') + + const apiKeyConstraints = [ + { + apiKeyIndex: { + equals: sha1APIKeyIndex, + }, + }, + { + apiKeyIndex: { + equals: sha256APIKeyIndex, + }, + }, + ] try { const where: Where = {} if (collectionConfig.auth?.verify) { where.and = [ { - apiKeyIndex: { - equals: apiKeyIndex, - }, + or: apiKeyConstraints, }, { _verified: { @@ -30,9 +48,7 @@ export const APIKeyAuthentication = }, ] } else { - where.apiKeyIndex = { - equals: apiKeyIndex, - } + where.or = apiKeyConstraints } const userQuery = await payload.find({ diff --git a/test/auth/int.spec.ts b/test/auth/int.spec.ts index ecfa5f1ad9..4e52206a82 100644 --- a/test/auth/int.spec.ts +++ b/test/auth/int.spec.ts @@ -8,6 +8,7 @@ import type { User, } from 'payload' +import crypto from 'crypto' import { jwtDecode } from 'jwt-decode' import path from 'path' import { email as emailValidation } from 'payload/shared' @@ -15,6 +16,7 @@ import { fileURLToPath } from 'url' import { v4 as uuid } from 'uuid' import type { NextRESTClient } from '../helpers/NextRESTClient.js' +import type { ApiKey } from './payload-types.js' import { devUser } from '../credentials.js' import { initPayloadInt } from '../helpers/initPayloadInt.js' @@ -829,6 +831,37 @@ describe('Auth', () => { expect(fail.status).toStrictEqual(404) }) + it('should allow authentication with an API key saved with sha1', async () => { + const usersQuery = await payload.find({ + collection: apiKeysSlug, + }) + + const [user] = usersQuery.docs as [ApiKey] + + const sha1Index = crypto + .createHmac('sha256', payload.secret) + .update(user.apiKey as string) + .digest('hex') + + await payload.db.updateOne({ + collection: apiKeysSlug, + data: { + apiKeyIndex: sha1Index, + }, + id: user.id, + }) + + const response = await restClient + .GET(`/${apiKeysSlug}/${user?.id}`, { + headers: { + Authorization: `${apiKeysSlug} API-Key ${user?.apiKey}`, + }, + }) + .then((res) => res.json()) + + expect(response.id).toStrictEqual(user.id) + }) + it('should not remove an API key from a user when updating other fields', async () => { const apiKey = uuid() const user = await payload.create({