diff --git a/packages/backend-core/src/Base.ts b/packages/backend-core/src/Base.ts index 57df842de2..7323e448d4 100644 --- a/packages/backend-core/src/Base.ts +++ b/packages/backend-core/src/Base.ts @@ -30,10 +30,6 @@ export type VerifySessionTokenOptions = { jwtKey?: string; }; -const verifySessionTokenDefaultOptions: VerifySessionTokenOptions = { - authorizedParties: [], -}; - type AuthState = { status: AuthStatus; session?: Session; @@ -108,7 +104,7 @@ export class Base { */ verifySessionToken = async ( token: string, - { authorizedParties, jwtKey }: VerifySessionTokenOptions = verifySessionTokenDefaultOptions, + { authorizedParties, jwtKey }: VerifySessionTokenOptions, ): Promise => { /** * Priority of JWT key search diff --git a/packages/edge/src/vercel-edge/index.ts b/packages/edge/src/vercel-edge/index.ts index e1796a9fc8..48326a1229 100644 --- a/packages/edge/src/vercel-edge/index.ts +++ b/packages/edge/src/vercel-edge/index.ts @@ -68,12 +68,12 @@ export function withEdgeMiddlewareAuth< export function withEdgeMiddlewareAuth( handler: any, options: any = { - authorizedParties: [], loadSession: false, loadUser: false, }, ): any { return async function clerkAuth(req: NextRequest, event: NextFetchEvent) { + const { loadUser, loadSession, jwtKey, authorizedParties } = options; const cookieToken = req.cookies['__session']; const headerToken = req.headers.get('authorization'); const { status, interstitial, sessionClaims } = await vercelEdgeBase.getAuthState({ @@ -86,7 +86,8 @@ export function withEdgeMiddlewareAuth( forwardedPort: req.headers.get('x-forwarded-port'), forwardedHost: req.headers.get('x-forwarded-host'), referrer: req.headers.get('referrer'), - authorizedParties: options.authorizedParties, + authorizedParties, + jwtKey, fetchInterstitial, }); @@ -106,8 +107,8 @@ export function withEdgeMiddlewareAuth( const userId = sessionClaims!.sub; const [user, session] = await Promise.all([ - options.loadUser ? ClerkAPI.users.getUser(userId) : Promise.resolve(undefined), - options.loadSession ? ClerkAPI.sessions.getSession(sessionId) : Promise.resolve(undefined), + loadUser ? ClerkAPI.users.getUser(userId) : Promise.resolve(undefined), + loadSession ? ClerkAPI.sessions.getSession(sessionId) : Promise.resolve(undefined), ]); const getToken = createGetToken({ diff --git a/packages/edge/src/vercel-edge/types.ts b/packages/edge/src/vercel-edge/types.ts index 793b936a63..4446dd5c66 100644 --- a/packages/edge/src/vercel-edge/types.ts +++ b/packages/edge/src/vercel-edge/types.ts @@ -6,6 +6,7 @@ export type WithEdgeMiddlewareAuthOptions = { loadUser?: boolean; loadSession?: boolean; authorizedParties?: string[]; + jwtKey?: string; }; export type WithEdgeMiddlewareAuthCallback = ( diff --git a/packages/nextjs/src/middleware/types.ts b/packages/nextjs/src/middleware/types.ts index 240bed6cf8..75f98ed257 100644 --- a/packages/nextjs/src/middleware/types.ts +++ b/packages/nextjs/src/middleware/types.ts @@ -8,6 +8,8 @@ export type Awaited = T extends PromiseLike ? U : T; export type WithServerSideAuthOptions = { loadUser?: boolean; loadSession?: boolean; + jwtKey?: string; + authorizedParties?: string[]; }; export type WithServerSideAuthCallback = (context: ContextWithAuth) => Return; diff --git a/packages/nextjs/src/middleware/utils/getAuthData.ts b/packages/nextjs/src/middleware/utils/getAuthData.ts index b4aefbabc9..ba5e47d041 100644 --- a/packages/nextjs/src/middleware/utils/getAuthData.ts +++ b/packages/nextjs/src/middleware/utils/getAuthData.ts @@ -12,7 +12,7 @@ export async function getAuthData( opts: WithServerSideAuthOptions = {}, ): Promise { const { headers, cookies } = ctx.req; - const { loadSession, loadUser } = opts; + const { loadSession, loadUser, jwtKey, authorizedParties } = opts; try { const cookieToken = cookies['__session']; @@ -28,6 +28,8 @@ export async function getAuthData( referrer: headers.referer, userAgent: headers['user-agent'] as string, fetchInterstitial: () => Clerk.fetchInterstitial(), + jwtKey, + authorizedParties, }); if (status === AuthStatus.Interstitial) { diff --git a/packages/remix/src/ssr/getAuthData.ts b/packages/remix/src/ssr/getAuthData.ts index 975282f18b..dafe4d6e73 100644 --- a/packages/remix/src/ssr/getAuthData.ts +++ b/packages/remix/src/ssr/getAuthData.ts @@ -20,7 +20,7 @@ export async function getAuthData( req: Request, opts: RootAuthLoaderOptions = {}, ): Promise<{ authData: AuthData | null; showInterstitial?: boolean }> { - const { loadSession, loadUser } = opts; + const { loadSession, loadUser, jwtKey, authorizedParties } = opts; const { headers } = req; const cookies = parseCookies(req); @@ -38,6 +38,8 @@ export async function getAuthData( referrer: headers.get('referer'), userAgent: headers.get('user-agent') as string, fetchInterstitial: () => Promise.resolve(''), + authorizedParties, + jwtKey, }); if (status === AuthStatus.Interstitial) { diff --git a/packages/remix/src/ssr/types.ts b/packages/remix/src/ssr/types.ts index 7b5dbd7ec1..67f2bc2d87 100644 --- a/packages/remix/src/ssr/types.ts +++ b/packages/remix/src/ssr/types.ts @@ -8,6 +8,8 @@ export type RootAuthLoaderOptions = { frontendApi?: string; loadUser?: boolean; loadSession?: boolean; + jwtKey?: string; + authorizedParties?: []; }; export type RootAuthLoaderCallback = ( diff --git a/packages/sdk-node/README.md b/packages/sdk-node/README.md index 64acfd4713..2cde579673 100644 --- a/packages/sdk-node/README.md +++ b/packages/sdk-node/README.md @@ -713,6 +713,32 @@ export clerk.withAuth(handler); export clerk.requireAuth(handler); ``` +## Networkless token verification using the JWT verification key + +Clerk's JWT session token can be verified in a networkless manner using the JWT verification key. By default Clerk will use our JWKs endpoint to fetch and cache the key for any subsequent verification. If you use the `CLERK_JWT_KEY` environment variable to supply the key, Clerk will pick it up and do networkless verification for session tokens using it. + +To learn more about Clerk's token verification you can find more information on our [documentation](https://docs.clerk.dev/popular-guides/validating-session-tokens). + +The value of the JWT verification key can also be added on the instance level or on any single middleware call e.g. + +```ts +import { withAuth } from '@clerk/clerk-sdk-node'; + +const handler = (req, res) => { + // ... +}; + +withAuth(handler, { jwtKey: 'my_clerk_public_key' }); +``` + +Custom instance initialization: + +```ts +import Clerk from '@clerk/clerk-sdk-node/instance'; + +const clerk = new Clerk({ jwtKey: 'my_clerk_public_key' }); +``` + ## Validate the Authorized Party of a session token Clerk's JWT session token, contains the azp claim, which equals the Origin of the request during token generation. You can provide the middlewares with a list of whitelisted origins to verify against, to protect your application of the subdomain cookie leaking attack. You can find an example below: diff --git a/packages/sdk-node/src/Clerk.ts b/packages/sdk-node/src/Clerk.ts index 798a1092b3..88ee211075 100644 --- a/packages/sdk-node/src/Clerk.ts +++ b/packages/sdk-node/src/Clerk.ts @@ -25,6 +25,7 @@ import { Crypto, CryptoKey } from '@peculiar/webcrypto'; import { decodeBase64, toSPKIDer } from './utils/crypto'; const defaultApiKey = process.env.CLERK_API_KEY || ''; +const defaultJWTKey = process.env.CLERK_JWT_KEY; const defaultApiVersion = process.env.CLERK_API_VERSION || 'v1'; const defaultServerApiUrl = process.env.CLERK_API_URL || 'https://api.clerk.dev'; @@ -34,6 +35,7 @@ const packageRepo = 'https://github.com/clerkinc/clerk-sdk-node'; export type MiddlewareOptions = { onError?: Function; authorizedParties?: string[]; + jwtKey?: string; }; export type WithAuthProp = T & { @@ -67,6 +69,7 @@ const verifySignature = async ( export default class Clerk extends ClerkBackendAPI { base: Base; + jwtKey?: string; httpOptions: OptionsOfUnknownResponseBody; _jwksClient: JwksClient; @@ -76,12 +79,14 @@ export default class Clerk extends ClerkBackendAPI { constructor({ apiKey = defaultApiKey, + jwtKey = defaultJWTKey, serverApiUrl = defaultServerApiUrl, apiVersion = defaultApiVersion, httpOptions = {}, jwksCacheMaxAge = JWKS_MAX_AGE, }: { apiKey?: string; + jwtKey?: string; serverApiUrl?: string; apiVersion?: string; httpOptions?: OptionsOfUnknownResponseBody; @@ -122,6 +127,7 @@ export default class Clerk extends ClerkBackendAPI { } this.httpOptions = httpOptions; + this.jwtKey = jwtKey; this._jwksClient = jwks({ jwksUri: `${serverApiUrl}/${apiVersion}/jwks`, @@ -163,7 +169,7 @@ export default class Clerk extends ClerkBackendAPI { importKey, verifySignature, decodeBase64, - process.env.CLERK_JWT_KEY ? undefined : loadCryptoKey + loadCryptoKey ); } @@ -230,7 +236,7 @@ export default class Clerk extends ClerkBackendAPI { } expressWithAuth( - { onError, authorizedParties }: MiddlewareOptions = { + { onError, authorizedParties, jwtKey }: MiddlewareOptions = { onError: this.defaultOnError, } ): (req: Request, res: Response, next: NextFunction) => Promise { @@ -261,6 +267,7 @@ export default class Clerk extends ClerkBackendAPI { referrer: req.headers.referer, userAgent: req.headers['user-agent'] as string, authorizedParties, + jwtKey: jwtKey || this.jwtKey, fetchInterstitial: () => this.fetchInterstitial(), }); @@ -315,11 +322,11 @@ export default class Clerk extends ClerkBackendAPI { } expressRequireAuth( - { onError, authorizedParties }: MiddlewareOptions = { + options: MiddlewareOptions = { onError: this.strictOnError, } ) { - return this.expressWithAuth({ onError, authorizedParties }); + return this.expressWithAuth(options); } // Credits to https://nextjs.org/docs/api-routes/api-middlewares @@ -342,7 +349,7 @@ export default class Clerk extends ClerkBackendAPI { // Set the session on the request and then call provided handler withAuth( handler: Function, - { onError, authorizedParties }: MiddlewareOptions = { + options: MiddlewareOptions = { onError: this.defaultOnError, } ) { @@ -352,11 +359,7 @@ export default class Clerk extends ClerkBackendAPI { next?: NextFunction ) => { try { - await this._runMiddleware( - req, - res, - this.expressWithAuth({ onError, authorizedParties }) - ); + await this._runMiddleware(req, res, this.expressWithAuth(options)); return handler(req, res, next); } catch (error) { // @ts-ignore @@ -376,10 +379,10 @@ export default class Clerk extends ClerkBackendAPI { // Stricter version, short-circuits if session can't be determined requireAuth( handler: Function, - { onError, authorizedParties }: MiddlewareOptions = { + options: MiddlewareOptions = { onError: this.strictOnError, } ) { - return this.withAuth(handler, { onError, authorizedParties }); + return this.withAuth(handler, options); } } diff --git a/packages/sdk-node/src/__tests__/instance.test.ts b/packages/sdk-node/src/__tests__/instance.test.ts index af64a7f188..422a905dad 100644 --- a/packages/sdk-node/src/__tests__/instance.test.ts +++ b/packages/sdk-node/src/__tests__/instance.test.ts @@ -1,4 +1,5 @@ const TEST_API_KEY = 'TEST_API_KEY'; +const TEST_JWT_KEY = 'TEST_JWT_KEY'; describe('Custom Clerk instance initialization', () => { test('throw error when initialized without apiKey', () => { @@ -26,17 +27,21 @@ describe('Custom Clerk instance initialization', () => { test('custom keys overrides process env and default params', () => { jest.resetModules(); process.env.CLERK_API_KEY = TEST_API_KEY; + process.env.CLERK_JWT_KEY = TEST_JWT_KEY; const Clerk = require('../instance').default; expect(() => { - const customKey = 'custom_key'; + const customAPIKey = 'custom_api_key'; + const customJWTKey = 'custom_jwt_key'; const customAPIVersion = 'v0'; const customAPIUrl = 'https://customdomain.com'; const instance = new Clerk({ - apiKey: customKey, + apiKey: customAPIKey, + jwtKey: customJWTKey, serverApiUrl: customAPIUrl, apiVersion: customAPIVersion, }); - expect(instance._restClient.apiKey).toBe(customKey); + expect(instance._restClient.apiKey).toBe(customAPIKey); + expect(instance.jwtKey).toBe(customJWTKey); expect(instance._restClient.serverApiUrl).toBe(customAPIUrl); expect(instance._restClient.apiVersion).toBe(customAPIVersion); }).not.toThrow(Error);