Skip to content

Commit b9f32e0

Browse files
authored
chore(nextjs): Improve machine auth verification calls (#6367)
1 parent 8feb59b commit b9f32e0

File tree

8 files changed

+251
-153
lines changed

8 files changed

+251
-153
lines changed

.changeset/eight-impalas-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/nextjs": patch
3+
---
4+
5+
Improved machine auth verification within API routes

packages/nextjs/src/app-router/server/auth.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,11 @@ export const auth: AuthFn = (async (options?: AuthOptions) => {
179179
});
180180
};
181181

182-
return Object.assign(authObject, { redirectToSignIn, redirectToSignUp });
182+
if (authObject.tokenType === TokenType.SessionToken) {
183+
return Object.assign(authObject, { redirectToSignIn, redirectToSignUp });
184+
}
185+
186+
return authObject;
183187
}) as AuthFn;
184188

185189
auth.protect = async (...args: any[]) => {
Lines changed: 124 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,188 @@
1-
import type { AuthenticatedMachineObject, SignedOutAuthObject } from '@clerk/backend/internal';
2-
import { constants, verifyMachineAuthToken } from '@clerk/backend/internal';
1+
import type { MachineAuthObject } from '@clerk/backend';
2+
import type { AuthenticatedMachineObject, MachineTokenType, SignedOutAuthObject } from '@clerk/backend/internal';
3+
import { constants } from '@clerk/backend/internal';
34
import { NextRequest } from 'next/server';
45
import { beforeEach, describe, expect, it, vi } from 'vitest';
56

6-
import { getAuthDataFromRequestAsync, getAuthDataFromRequestSync } from '../data/getAuthDataFromRequest';
7+
import { getAuthDataFromRequest } from '../data/getAuthDataFromRequest';
8+
import { encryptClerkRequestData } from '../utils';
79

8-
vi.mock('@clerk/backend/internal', async () => {
9-
const actual = await vi.importActual('@clerk/backend/internal');
10+
vi.mock(import('../constants.js'), async importOriginal => {
11+
const actual = await importOriginal();
1012
return {
1113
...actual,
12-
verifyMachineAuthToken: vi.fn(),
14+
ENCRYPTION_KEY: 'encryption-key',
15+
PUBLISHABLE_KEY: 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA',
16+
SECRET_KEY: 'sk_test_xxxxxxxxxxxxxxxxxx',
1317
};
1418
});
1519

1620
type MockRequestParams = {
1721
url: string;
1822
appendDevBrowserCookie?: boolean;
1923
method?: string;
20-
headers?: any;
24+
headers?: Headers;
25+
machineAuthObject?: Partial<MachineAuthObject<MachineTokenType>>;
2126
};
2227

2328
const mockRequest = (params: MockRequestParams) => {
24-
const { url, appendDevBrowserCookie = false, method = 'GET', headers = new Headers() } = params;
29+
const { url, appendDevBrowserCookie = false, method = 'GET', headers = new Headers(), machineAuthObject } = params;
2530
const headersWithCookie = new Headers(headers);
31+
2632
if (appendDevBrowserCookie) {
2733
headersWithCookie.append('cookie', '__clerk_db_jwt=test_jwt');
2834
}
35+
36+
// Add encrypted auth object header if provided
37+
if (machineAuthObject) {
38+
const encryptedData = encryptClerkRequestData(
39+
{}, // requestData
40+
{}, // keylessModeKeys
41+
// @ts-expect-error - mock machine auth object
42+
machineAuthObject,
43+
);
44+
if (encryptedData) {
45+
headersWithCookie.set(constants.Headers.ClerkRequestData, encryptedData);
46+
}
47+
}
48+
2949
return new NextRequest(new URL(url, 'https://www.clerk.com').toString(), { method, headers: headersWithCookie });
3050
};
3151

32-
const machineTokenErrorMock = [
33-
{
34-
message: 'Token type mismatch',
35-
code: 'token-invalid',
36-
status: 401,
37-
name: 'MachineTokenVerificationError',
38-
getFullMessage: () => 'Token type mismatch',
39-
},
40-
];
41-
42-
describe('getAuthDataFromRequestAsync', () => {
52+
// Helper function to create mock machine auth objects
53+
const createMockMachineAuthObject = <T extends MachineTokenType>(data: Partial<MachineAuthObject<T>>) => data;
54+
55+
describe('getAuthDataFromRequest', () => {
4356
beforeEach(() => {
4457
vi.clearAllMocks();
4558
});
4659

47-
it('returns invalid token auth object when token type does not match any in acceptsToken array', async () => {
60+
it('returns invalid token auth object when token type does not match any in acceptsToken array', () => {
61+
const machineAuthObject = createMockMachineAuthObject({
62+
tokenType: 'api_key',
63+
isAuthenticated: true,
64+
});
65+
4866
const req = mockRequest({
4967
url: '/api/protected',
5068
headers: new Headers({
5169
[constants.Headers.Authorization]: 'Bearer ak_xxx',
5270
}),
71+
machineAuthObject,
5372
});
5473

55-
const auth = await getAuthDataFromRequestAsync(req, {
74+
const auth = getAuthDataFromRequest(req, {
5675
acceptsToken: ['machine_token', 'oauth_token', 'session_token'],
5776
});
5877

5978
expect(auth.tokenType).toBeNull();
6079
expect(auth.isAuthenticated).toBe(false);
6180
});
6281

63-
it('returns unauthenticated auth object when token type does not match single acceptsToken', async () => {
82+
it('handles mixed token types in acceptsToken array', () => {
83+
const machineAuthObject = createMockMachineAuthObject({
84+
tokenType: 'api_key',
85+
isAuthenticated: true,
86+
id: 'ak_id123',
87+
});
88+
89+
const req = mockRequest({
90+
url: '/api/protected',
91+
headers: new Headers({
92+
[constants.Headers.Authorization]: 'Bearer ak_xxx',
93+
}),
94+
machineAuthObject,
95+
});
96+
97+
const auth = getAuthDataFromRequest(req, {
98+
acceptsToken: ['api_key', 'session_token'],
99+
});
100+
101+
expect(auth.tokenType).toBe('api_key');
102+
expect(auth.isAuthenticated).toBe(true);
103+
});
104+
105+
it('falls back to session logic when machine token is not accepted', () => {
106+
const machineAuthObject = createMockMachineAuthObject({
107+
tokenType: 'api_key',
108+
isAuthenticated: true,
109+
});
110+
111+
const req = mockRequest({
112+
url: '/api/protected',
113+
headers: new Headers({
114+
[constants.Headers.Authorization]: 'Bearer ak_xxx',
115+
}),
116+
machineAuthObject,
117+
});
118+
119+
const auth = getAuthDataFromRequest(req, {
120+
acceptsToken: 'session_token',
121+
});
122+
123+
expect(auth.tokenType).toBe('session_token');
124+
expect(auth.isAuthenticated).toBe(false);
125+
});
126+
127+
it('returns unauthenticated auth object when token type does not match single acceptsToken', () => {
128+
const machineAuthObject = createMockMachineAuthObject({
129+
tokenType: 'api_key',
130+
isAuthenticated: true,
131+
});
132+
64133
const req = mockRequest({
65134
url: '/api/protected',
66135
headers: new Headers({
67136
[constants.Headers.Authorization]: 'Bearer ak_xxx',
68137
}),
138+
machineAuthObject,
69139
});
70140

71-
const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: 'oauth_token' });
141+
const auth = getAuthDataFromRequest(req, { acceptsToken: 'oauth_token' });
72142

73143
expect(auth.tokenType).toBe('oauth_token');
74144
expect(auth.isAuthenticated).toBe(false);
75145
});
76146

77-
it('returns authenticated auth object for any valid token type', async () => {
78-
vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({
79-
data: { id: 'ak_id123', subject: 'user_12345' } as any,
147+
it('returns authenticated auth object for any valid token type', () => {
148+
const machineAuthObject = createMockMachineAuthObject({
80149
tokenType: 'api_key',
81-
errors: undefined,
150+
id: 'ak_id123',
151+
isAuthenticated: true,
82152
});
83153

84154
const req = mockRequest({
85155
url: '/api/protected',
86156
headers: new Headers({
87157
[constants.Headers.Authorization]: 'Bearer ak_xxx',
88158
}),
159+
machineAuthObject,
89160
});
90161

91-
const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: 'any' });
162+
const auth = getAuthDataFromRequest(req, { acceptsToken: 'any' });
92163

93164
expect(auth.tokenType).toBe('api_key');
94165
expect((auth as AuthenticatedMachineObject<'api_key'>).id).toBe('ak_id123');
95166
expect(auth.isAuthenticated).toBe(true);
96167
});
97168

98-
it('returns authenticated object when token type exists in acceptsToken array', async () => {
99-
vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({
100-
data: { id: 'ak_id123', subject: 'user_12345' } as any,
169+
it('returns authenticated object when token type exists in acceptsToken array', () => {
170+
const machineAuthObject = createMockMachineAuthObject({
101171
tokenType: 'api_key',
102-
errors: undefined,
172+
id: 'ak_id123',
173+
subject: 'user_12345',
174+
isAuthenticated: true,
103175
});
104176

105177
const req = mockRequest({
106178
url: '/api/protected',
107179
headers: new Headers({
108-
[constants.Headers.Authorization]: 'Bearer ak_secret123',
180+
[constants.Headers.Authorization]: 'Bearer ak_xxx',
109181
}),
182+
machineAuthObject,
110183
});
111184

112-
const auth = await getAuthDataFromRequestAsync(req, {
185+
const auth = getAuthDataFromRequest(req, {
113186
acceptsToken: ['api_key', 'machine_token'],
114187
});
115188

@@ -136,21 +209,22 @@ describe('getAuthDataFromRequestAsync', () => {
136209
},
137210
])(
138211
'returns authenticated $tokenType object when token is valid and acceptsToken is $tokenType',
139-
async ({ tokenType, token, data }) => {
140-
vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({
141-
data: data as any,
212+
({ tokenType, token, data }) => {
213+
const machineAuthObject = createMockMachineAuthObject({
142214
tokenType,
143-
errors: undefined,
215+
isAuthenticated: true,
216+
...data,
144217
});
145218

146219
const req = mockRequest({
147220
url: '/api/protected',
148221
headers: new Headers({
149222
[constants.Headers.Authorization]: `Bearer ${token}`,
150223
}),
224+
machineAuthObject,
151225
});
152226

153-
const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType });
227+
const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType });
154228

155229
expect(auth.tokenType).toBe(tokenType);
156230
expect(auth.isAuthenticated).toBe(true);
@@ -173,56 +247,55 @@ describe('getAuthDataFromRequestAsync', () => {
173247
token: 'mt_123',
174248
data: undefined,
175249
},
176-
])('returns unauthenticated $tokenType object when token is invalid', async ({ tokenType, token, data }) => {
177-
vi.mocked(verifyMachineAuthToken).mockResolvedValueOnce({
178-
data: data as any,
250+
])('returns unauthenticated $tokenType object when token is invalid', ({ tokenType, token }) => {
251+
const machineAuthObject = createMockMachineAuthObject({
179252
tokenType,
180-
errors: machineTokenErrorMock as any,
253+
isAuthenticated: false,
181254
});
182255

256+
// For invalid tokens, we don't include encrypted auth object
183257
const req = mockRequest({
184258
url: '/api/protected',
185259
headers: new Headers({
186260
[constants.Headers.Authorization]: `Bearer ${token}`,
187261
}),
262+
machineAuthObject,
188263
});
189264

190-
const auth = await getAuthDataFromRequestAsync(req, { acceptsToken: tokenType });
265+
const auth = getAuthDataFromRequest(req, { acceptsToken: tokenType });
191266

192267
expect(auth.tokenType).toBe(tokenType);
193268
expect(auth.isAuthenticated).toBe(false);
194269
});
195270

196-
it('falls back to session token handling', async () => {
271+
it('falls back to session token handling when no encrypted auth object is present', () => {
197272
const req = mockRequest({
198273
url: '/api/protected',
199274
headers: new Headers({
200275
[constants.Headers.Authorization]: 'Bearer session_xxx',
201276
}),
202277
});
203278

204-
const auth = await getAuthDataFromRequestAsync(req);
279+
const auth = getAuthDataFromRequest(req);
205280
expect(auth.tokenType).toBe('session_token');
206281
expect((auth as SignedOutAuthObject).userId).toBeNull();
207282
expect(auth.isAuthenticated).toBe(false);
208283
});
209-
});
210284

211-
describe('getAuthDataFromRequestSync', () => {
212-
it('only accepts session tokens', () => {
285+
it('only accepts session tokens when encrypted auth object is not present', () => {
213286
const req = mockRequest({
214287
url: '/api/protected',
215288
headers: new Headers({
216289
[constants.Headers.Authorization]: 'Bearer ak_123',
217290
}),
218291
});
219292

220-
const auth = getAuthDataFromRequestSync(req, {
293+
const auth = getAuthDataFromRequest(req, {
221294
acceptsToken: 'api_key',
222295
});
223296

224297
expect(auth.tokenType).toBe('session_token');
225-
expect(auth.userId).toBeNull();
298+
expect((auth as SignedOutAuthObject).userId).toBeNull();
226299
expect(auth.isAuthenticated).toBe(false);
227300
});
228301
});

packages/nextjs/src/server/buildClerkProps.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AuthObject, Organization, Session, User } from '@clerk/backend';
22
import { makeAuthObjectSerializable, stripPrivateDataFromObject } from '@clerk/backend/internal';
33

4-
import { getAuthDataFromRequestSync } from './data/getAuthDataFromRequest';
4+
import { getAuthDataFromRequest } from './data/getAuthDataFromRequest';
55
import type { RequestLike } from './types';
66

77
type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; organization?: Organization | null };
@@ -59,7 +59,7 @@ export const buildClerkProps: BuildClerkProps = (req, initialState = {}) => {
5959
};
6060

6161
export function getDynamicAuthData(req: RequestLike, initialState = {}) {
62-
const authObject = getAuthDataFromRequestSync(req);
62+
const authObject = getAuthDataFromRequest(req);
6363

6464
return makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initialState })) as AuthObject;
6565
}

packages/nextjs/src/server/clerkMiddleware.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
getAuthObjectForAcceptedToken,
1616
isMachineTokenByPrefix,
1717
isTokenTypeAccepted,
18+
makeAuthObjectSerializable,
1819
TokenType,
1920
} from '@clerk/backend/internal';
2021
import { parsePublishableKey } from '@clerk/shared/keys';
@@ -265,7 +266,14 @@ export const clerkMiddleware = ((...args: unknown[]): NextMiddleware | NextMiddl
265266
}
266267
: {};
267268

268-
decorateRequest(clerkRequest, handlerResult, requestState, resolvedParams, keylessKeysForRequestData);
269+
decorateRequest(
270+
clerkRequest,
271+
handlerResult,
272+
requestState,
273+
resolvedParams,
274+
keylessKeysForRequestData,
275+
authObject.tokenType === 'session_token' ? null : makeAuthObjectSerializable(authObject),
276+
);
269277

270278
return handlerResult;
271279
});

0 commit comments

Comments
 (0)