Skip to content

Commit

Permalink
feat(backend-core,clerk-sdk-node,nextjs,remix): Add injected jwtKey o…
Browse files Browse the repository at this point in the history
…ption
  • Loading branch information
igneel64 committed Mar 22, 2022
1 parent 8b318cc commit 53e56e7
Show file tree
Hide file tree
Showing 10 changed files with 66 additions and 26 deletions.
6 changes: 1 addition & 5 deletions packages/backend-core/src/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,6 @@ export type VerifySessionTokenOptions = {
jwtKey?: string;
};

const verifySessionTokenDefaultOptions: VerifySessionTokenOptions = {
authorizedParties: [],
};

type AuthState = {
status: AuthStatus;
session?: Session;
Expand Down Expand Up @@ -108,7 +104,7 @@ export class Base {
*/
verifySessionToken = async (
token: string,
{ authorizedParties, jwtKey }: VerifySessionTokenOptions = verifySessionTokenDefaultOptions,
{ authorizedParties, jwtKey }: VerifySessionTokenOptions,
): Promise<JWTPayload> => {
/**
* Priority of JWT key search
Expand Down
9 changes: 5 additions & 4 deletions packages/edge/src/vercel-edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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,
});

Expand All @@ -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({
Expand Down
1 change: 1 addition & 0 deletions packages/edge/src/vercel-edge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type WithEdgeMiddlewareAuthOptions = {
loadUser?: boolean;
loadSession?: boolean;
authorizedParties?: string[];
jwtKey?: string;
};

export type WithEdgeMiddlewareAuthCallback<Return, Options> = (
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/src/middleware/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type Awaited<T> = T extends PromiseLike<infer U> ? U : T;
export type WithServerSideAuthOptions = {
loadUser?: boolean;
loadSession?: boolean;
jwtKey?: string;
authorizedParties?: string[];
};

export type WithServerSideAuthCallback<Return, Options> = (context: ContextWithAuth<Options>) => Return;
Expand Down
4 changes: 3 additions & 1 deletion packages/nextjs/src/middleware/utils/getAuthData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function getAuthData(
opts: WithServerSideAuthOptions = {},
): Promise<AuthData | null> {
const { headers, cookies } = ctx.req;
const { loadSession, loadUser } = opts;
const { loadSession, loadUser, jwtKey, authorizedParties } = opts;

try {
const cookieToken = cookies['__session'];
Expand All @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion packages/remix/src/ssr/getAuthData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/remix/src/ssr/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type RootAuthLoaderOptions = {
frontendApi?: string;
loadUser?: boolean;
loadSession?: boolean;
jwtKey?: string;
authorizedParties?: [];
};

export type RootAuthLoaderCallback<Options> = (
Expand Down
26 changes: 26 additions & 0 deletions packages/sdk-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
27 changes: 15 additions & 12 deletions packages/sdk-node/src/Clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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> = T & {
Expand Down Expand Up @@ -67,6 +69,7 @@ const verifySignature = async (

export default class Clerk extends ClerkBackendAPI {
base: Base;
jwtKey?: string;
httpOptions: OptionsOfUnknownResponseBody;

_jwksClient: JwksClient;
Expand All @@ -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;
Expand Down Expand Up @@ -122,6 +127,7 @@ export default class Clerk extends ClerkBackendAPI {
}

this.httpOptions = httpOptions;
this.jwtKey = jwtKey;

this._jwksClient = jwks({
jwksUri: `${serverApiUrl}/${apiVersion}/jwks`,
Expand Down Expand Up @@ -163,7 +169,7 @@ export default class Clerk extends ClerkBackendAPI {
importKey,
verifySignature,
decodeBase64,
process.env.CLERK_JWT_KEY ? undefined : loadCryptoKey
loadCryptoKey
);
}

Expand Down Expand Up @@ -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<void> {
Expand Down Expand Up @@ -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(),
});

Expand Down Expand Up @@ -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
Expand All @@ -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,
}
) {
Expand All @@ -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
Expand All @@ -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);
}
}
11 changes: 8 additions & 3 deletions packages/sdk-node/src/__tests__/instance.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit 53e56e7

Please sign in to comment.