perf: use getClaims for API endpoint auth (#39311)

Use getClaims instead of getUser which avoids a network call to GoTrue to validate the user in case of asymmetric keys - this shaves off a good amount of latency for every API call.
This commit is contained in:
Kevin Grüneberg authored and GitHub committed 2025-10-08 09:59:27 +08:00
1 parent 1f245734ae
commit f0436716f9
5 files changed
+30 -42

No files matched your search

+11 -11
View File
@@ -3,9 +3,9 @@ import { apiAuthenticate } from './apiAuthenticate'
const mocks = vi.hoisted(() => {
return {
getAuthUser: vi.fn().mockResolvedValue({
user: {
id: 'test-gotrue-id',
getUserClaims: vi.fn().mockResolvedValue({
claims: {
sub: 'test-gotrue-id',
email: 'test@example.com',
},
error: null,
@@ -14,7 +14,7 @@ const mocks = vi.hoisted(() => {
})
vi.mock('lib/gotrue', () => ({
getAuthUser: mocks.getAuthUser,
getUserClaims: mocks.getUserClaims,
}))
describe('apiAuthenticate', () => {
@@ -29,9 +29,9 @@ describe('apiAuthenticate', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.getAuthUser.mockResolvedValue({
user: {
id: 'test-gotrue-id',
mocks.getUserClaims.mockResolvedValue({
claims: {
sub: 'test-gotrue-id',
email: 'test@example.com',
},
error: null,
@@ -45,8 +45,8 @@ describe('apiAuthenticate', () => {
})
it('should return error when auth user fetch fails', async () => {
mocks.getAuthUser.mockResolvedValue({
user: null,
mocks.getUserClaims.mockResolvedValue({
claims: null,
error: new Error('Auth failed'),
})
@@ -55,8 +55,8 @@ describe('apiAuthenticate', () => {
})
it('should return error when user does not exist', async () => {
mocks.getAuthUser.mockResolvedValue({
user: null,
mocks.getUserClaims.mockResolvedValue({
claims: null,
error: null,
})
+12 -11
View File
@@ -1,6 +1,7 @@
import { getAuthUser } from 'lib/gotrue'
import type { JwtPayload } from '@supabase/supabase-js'
import { getUserClaims } from 'lib/gotrue'
import type { NextApiRequest, NextApiResponse } from 'next'
import type { ResponseError, SupaResponse, User } from 'types'
import type { ResponseError } from 'types'
/**
* Use this method on api routes to check if user is authenticated and having required permissions.
@@ -15,14 +16,14 @@ import type { ResponseError, SupaResponse, User } from 'types'
export async function apiAuthenticate(
req: NextApiRequest,
_res: NextApiResponse
): Promise<SupaResponse<User>> {
): Promise<JwtPayload | { error: ResponseError }> {
try {
const user = await fetchUser(req)
if (!user) {
const claims = await fetchUserClaims(req)
if (!claims) {
return { error: new Error('The user does not exist') }
}
return user
return claims
} catch (error) {
return { error: error as ResponseError }
}
@@ -32,19 +33,19 @@ export async function apiAuthenticate(
* @returns
* user with only id prop or detail object. It depends on requireUserDetail config
*/
async function fetchUser(req: NextApiRequest): Promise<any> {
const token = req.headers.authorization?.replace('Bearer ', '')
async function fetchUserClaims(req: NextApiRequest): Promise<JwtPayload> {
const token = req.headers.authorization?.replace(/bearer /i, '')
if (!token) {
throw new Error('missing access token')
}
const { user, error } = await getAuthUser(token)
const { claims, error } = await getUserClaims(token)
if (error) {
throw error
}
if (!user) {
if (!claims) {
throw new Error('The user does not exist')
}
return user
return claims
}
-3
View File
@@ -40,9 +40,6 @@ export default async function apiWrapper(
message: `Unauthorized: ${response.error.message}`,
},
})
} else {
// Attach user information to request parameters
;(req as any).user = response
}
}
-10
View File
@@ -28,14 +28,4 @@ describe('apiWrapper', () => {
expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes)
expect(apiAuthenticate).not.toHaveBeenCalled()
})
it('should attach user to request and call handler when authentication succeeds', async () => {
const mockUser = { id: '123', email: 'test@example.com' } as any as any
vi.mocked(apiAuthenticate).mockResolvedValue(mockUser)
await apiWrapper(mockReq, mockRes, mockHandler, { withAuth: true })
expect(mockReq.user).toEqual(mockUser)
expect(mockHandler).toHaveBeenCalledWith(mockReq, mockRes)
})
})
+7 -7
View File
@@ -1,3 +1,4 @@
import type { JwtPayload } from '@supabase/supabase-js'
import { getAccessToken, type User } from 'common/auth'
import { gotrueClient } from 'common/gotrue'
@@ -23,18 +24,17 @@ export const validateReturnTo = (
return safePathPattern.test(returnTo) ? returnTo : fallback
}
export const getAuthUser = async (token: String): Promise<any> => {
export const getUserClaims = async (
token: String
): Promise<{ error: any | null; claims: JwtPayload | null }> => {
try {
const {
data: { user },
error,
} = await auth.getUser(token.replace('Bearer ', ''))
const { data, error } = await auth.getClaims(token.replace(/bearer /i, ''))
if (error) throw error
return { user, error: null }
return { claims: data?.claims ?? null, error: null }
} catch (err) {
console.error(err)
return { user: null, error: err }
return { claims: null, error: err }
}
}