Skip to content

Commit ecb60da

Browse files
authored
feat(nextjs): Add signature verification of token passed from middleware (clerk#3121)
* feat(nextjs): Add signature verification of token passed from middleware * chore(repo): Adds changeset * chore(fastify): Update snapshot * chore(nextjs): Pin crypto-js version * chore(nextjs): Tweak error message
1 parent 701d3fd commit ecb60da

11 files changed

Lines changed: 85 additions & 7 deletions

File tree

.changeset/clean-deers-mate.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/backend': minor
3+
'@clerk/nextjs': minor
4+
---
5+
6+
Implement token signature verification when passing verified token from Next.js middleware to the application origin.

package-lock.json

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/backend/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const JWKS_CACHE_TTL_MS = 1000 * 60 * 60;
77

88
const Attributes = {
99
AuthToken: '__clerkAuthToken',
10+
AuthSignature: '__clerkAuthSignature',
1011
AuthStatus: '__clerkAuthStatus',
1112
AuthReason: '__clerkAuthReason',
1213
AuthMessage: '__clerkAuthMessage',
@@ -32,6 +33,7 @@ const QueryParameters = {
3233

3334
const Headers = {
3435
AuthToken: 'x-clerk-auth-token',
36+
AuthSignature: 'x-clerk-auth-signature',
3537
AuthStatus: 'x-clerk-auth-status',
3638
AuthReason: 'x-clerk-auth-reason',
3739
AuthMessage: 'x-clerk-auth-message',

packages/fastify/src/__tests__/__snapshots__/constants.test.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ exports[`constants from environment variables 1`] = `
1414
"Accept": "accept",
1515
"AuthMessage": "x-clerk-auth-message",
1616
"AuthReason": "x-clerk-auth-reason",
17+
"AuthSignature": "x-clerk-auth-signature",
1718
"AuthStatus": "x-clerk-auth-status",
1819
"AuthToken": "x-clerk-auth-token",
1920
"Authorization": "authorization",

packages/nextjs/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,11 +70,13 @@
7070
"@clerk/backend": "1.0.0-beta.33",
7171
"@clerk/clerk-react": "5.0.0-beta.36",
7272
"@clerk/shared": "2.0.0-beta.21",
73+
"crypto-js": "4.2.0",
7374
"path-to-regexp": "6.2.1",
7475
"tslib": "2.4.1"
7576
},
7677
"devDependencies": {
7778
"@clerk/types": "4.0.0-beta.25",
79+
"@types/crypto-js": "4.2.2",
7880
"@types/node": "^18.17.0",
7981
"@types/react": "*",
8082
"@types/react-dom": "*",

packages/nextjs/src/server/__tests__/createGetAuth.test.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { AuthStatus, constants } from '@clerk/backend/internal';
2+
import hmacSHA1 from 'crypto-js/hmac-sha1';
23
import { NextRequest } from 'next/server';
34

45
import { createGetAuth, getAuth } from '../createGetAuth';
56

7+
const mockSecretKey = 'sk_test_mock';
8+
69
// { alg: 'HS256' }.{ sub: 'user-id' }.sig
710
const mockToken = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLWlkIn0.0u5CllULtDVD9DUUmUMdJLbBCSNcnv4j3hCaPz4dNr8';
811
// { alg: 'HS256' }.{ sub: 'user-id-2' }.sig
912
const mockToken2 = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLWlkLTIifQ.K-mhz0Ber1Hfh2xCwmvsLwhZO_IKLtKt78KTHsecEas';
1013

14+
const mockTokenSignature = hmacSHA1(mockToken, 'sk_test_mock').toString();
15+
1116
describe('createGetAuth(opts)', () => {
1217
it('returns a getAuth function', () => {
1318
expect(createGetAuth({ debugLoggerName: 'test', noAuthStatusMessage: 'test' })).toBeInstanceOf(Function);
@@ -20,12 +25,13 @@ describe('getAuth(req)', () => {
2025
headers: new Headers({
2126
[constants.Headers.AuthStatus]: AuthStatus.SignedIn,
2227
[constants.Headers.AuthToken]: mockToken,
28+
[constants.Headers.AuthSignature]: mockTokenSignature,
2329
[constants.Headers.AuthMessage]: 'message',
2430
[constants.Headers.AuthReason]: 'reason',
2531
}),
2632
});
2733

28-
expect(getAuth(req).userId).toEqual('user-id');
34+
expect(getAuth(req, { secretKey: mockSecretKey }).userId).toEqual('user-id');
2935
});
3036

3137
it('parses and returns the token claims when signed out', () => {
@@ -45,13 +51,14 @@ describe('getAuth(req)', () => {
4551
headers: new Headers({
4652
[constants.Headers.AuthStatus]: AuthStatus.SignedIn,
4753
[constants.Headers.AuthToken]: mockToken,
54+
[constants.Headers.AuthSignature]: mockTokenSignature,
4855
[constants.Headers.AuthMessage]: 'message',
4956
[constants.Headers.AuthReason]: 'reason',
5057
Cookie: `__session=${mockToken2};`,
5158
}),
5259
});
5360

54-
expect(getAuth(req).userId).toEqual('user-id');
61+
expect(getAuth(req, { secretKey: mockSecretKey }).userId).toEqual('user-id');
5562
});
5663

5764
it('throws if auth status is not found', () => {
@@ -63,4 +70,18 @@ describe('getAuth(req)', () => {
6370

6471
expect(() => getAuth(req)).toThrowError();
6572
});
73+
74+
it('throws if signature does not match token', () => {
75+
const req = new NextRequest('https://www.clerk.com', {
76+
headers: new Headers({
77+
[constants.Headers.AuthStatus]: AuthStatus.SignedIn,
78+
[constants.Headers.AuthToken]: mockToken2,
79+
[constants.Headers.AuthSignature]: mockTokenSignature,
80+
[constants.Headers.AuthMessage]: 'message',
81+
[constants.Headers.AuthReason]: 'reason',
82+
}),
83+
});
84+
85+
expect(() => getAuth(req, { secretKey: mockSecretKey })).toThrowError();
86+
});
6687
});

packages/nextjs/src/server/authMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ const authMiddleware: AuthMiddleware = (...args: unknown[]) => {
219219
logger.debug(`Added ${constants.Headers.EnableDebug} on request`);
220220
}
221221

222-
const result = decorateRequest(clerkRequest, finalRes, requestState) || NextResponse.next();
222+
const result = decorateRequest(clerkRequest, finalRes, requestState, secretKey) || NextResponse.next();
223223

224224
if (requestState.headers) {
225225
requestState.headers.forEach((value, key) => {

packages/nextjs/src/server/clerkMiddleware.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]): any => {
130130
setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: 'true' });
131131
}
132132

133-
decorateRequest(clerkRequest, handlerResult, requestState);
133+
decorateRequest(clerkRequest, handlerResult, requestState, options.secretKey);
134134

135135
// TODO @nikos: we need to make this more generic
136136
// and move the logic in clerk/backend

packages/nextjs/src/server/createGetAuth.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { withLogger } from '../utils/debugLogger';
66
import { API_URL, API_VERSION, SECRET_KEY } from './constants';
77
import { getAuthAuthHeaderMissing } from './errors';
88
import type { RequestLike } from './types';
9-
import { getAuthKeyFromRequest, getCookie, getHeader } from './utils';
9+
import { assertTokenSignature, getAuthKeyFromRequest, getCookie, getHeader } from './utils';
1010

1111
export const createGetAuth = ({
1212
noAuthStatusMessage,
@@ -25,6 +25,7 @@ export const createGetAuth = ({
2525
// Then, we don't have to re-verify the JWT here,
2626
// we can just strip out the claims manually.
2727
const authToken = getAuthKeyFromRequest(req, 'AuthToken');
28+
const authSignature = getAuthKeyFromRequest(req, 'AuthSignature');
2829
const authMessage = getAuthKeyFromRequest(req, 'AuthMessage');
2930
const authReason = getAuthKeyFromRequest(req, 'AuthReason');
3031
const authStatus = getAuthKeyFromRequest(req, 'AuthStatus') as AuthStatus;
@@ -46,6 +47,8 @@ export const createGetAuth = ({
4647
logger.debug('Options debug', options);
4748

4849
if (authStatus === AuthStatus.SignedIn) {
50+
assertTokenSignature(authToken as string, options.secretKey, authSignature);
51+
4952
const jwt = decodeJwt(authToken as string);
5053

5154
logger.debug('JWT debug', jwt.raw.text);

packages/nextjs/src/server/errors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,5 @@ ${[apiRoutesText, publicRoutesText, ignoredRoutesText, afterAuthText]
9595
For additional information about middleware, please visit https://clerk.com/docs/nextjs/middleware
9696
(This log only appears in development mode, or if \`debug: true\` is passed to authMiddleware)`;
9797
};
98+
99+
export const authSignatureInvalid = `Clerk: Unable to verify request, this usually means the Clerk middleware did not run. Ensure Clerk's middleware is properly integrated and matches the current route. For more information, see: https://clerk.com/docs/nextjs/middleware. (code=auth_signature_invalid)`;

0 commit comments

Comments
 (0)