Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/true-bears-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/backend': patch
---

Improve JSDoc comments
4 changes: 4 additions & 0 deletions .typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,9 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"clerk-react/use-sign-in.mdx",
"clerk-react/use-sign-up.mdx",
"clerk-react/use-user.mdx",
"backend/verify-token-options.mdx",
"backend/verify-token.mdx",
"backend/verify-webhook-options.mdx",
"backend/verify-webhook.mdx",
]
`;
1 change: 1 addition & 0 deletions .typedoc/__tests__/file-structure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('Typedoc output', () => {

expect(folders).toMatchInlineSnapshot(`
[
"backend",
"clerk-react",
"nextjs",
"shared",
Expand Down
7 changes: 7 additions & 0 deletions .typedoc/custom-plugin.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,13 @@ function getCatchAllReplacements() {
pattern: /\*\*Default\*\* `([^`]+)`/g,
replace: 'Defaults to `$1`.',
},
{
/**
* By default, `@example` is output with "**Example** `value`". We want to capture the value and place it inside "Example: `value`."
*/
pattern: /\*\*Example\*\* `([^`]+)`/g,
replace: 'Example: `$1`.',
},
];
}

Expand Down
5 changes: 4 additions & 1 deletion packages/backend/src/jwt/verifyJwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ export function decodeJwt(token: string): JwtReturnType<Jwt, TokenVerificationEr
return { data };
}

/**
* @inline
*/
export type VerifyJwtOptions = {
/**
* A string or list of [audiences](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3). If passed, it is checked against the `aud` claim in the token.
Expand All @@ -103,7 +106,7 @@ export type VerifyJwtOptions = {
* An allowlist of origins to verify against, to protect your application from the subdomain cookie leaking attack.
* @example
* ```ts
* authorizedParties: ['http://localhost:3000', 'https://example.com']
* ['http://localhost:3000', 'https://example.com']
* ```
*/
authorizedParties?: string[];
Expand Down
1 change: 1 addition & 0 deletions packages/backend/src/tokens/__tests__/authObjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ describe('makeAuthObjectSerializable', () => {
const serializableAuthObject = makeAuthObjectSerializable(authObject);

for (const key in serializableAuthObject) {
// @ts-expect-error - Testing
expect(typeof serializableAuthObject[key]).not.toBe('function');
}
});
Expand Down
3 changes: 3 additions & 0 deletions packages/backend/src/tokens/__tests__/request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ const mockRequest = (headers = {}, requestUrl = 'http://clerk.com/path') => {
};

/* An otherwise bare state on a request. */
// @ts-expect-error - Testing
const mockOptions = (options?) => {
return {
secretKey: 'deadbeef',
Expand All @@ -249,10 +250,12 @@ const mockOptions = (options?) => {
} satisfies AuthenticateRequestOptions;
};

// @ts-expect-error - Testing
const mockRequestWithHeaderAuth = (headers?, requestUrl?) => {
return mockRequest({ authorization: `Bearer ${mockJwt}`, ...headers }, requestUrl);
};

// @ts-expect-error - Testing
const mockRequestWithCookies = (headers?, cookies = {}, requestUrl?) => {
const cookieStr = Object.entries(cookies)
.map(([k, v]) => `${k}=${v}`)
Expand Down
7 changes: 5 additions & 2 deletions packages/backend/src/tokens/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export function loadClerkJWKFromLocal(localKey?: string): JsonWebKey {
return getFromCache(LocalJwkKid);
}

/**
* @internal
*/
export type LoadClerkJWKFromRemoteOptions = {
/**
* @internal
Expand All @@ -94,15 +97,15 @@ export type LoadClerkJWKFromRemoteOptions = {
*/
jwksCacheTtlInMs?: number;
/**
* A flag to skip ignore cache and always fetch JWKS before each jwt verification.
* A flag to ignore the JWKS cache and always fetch JWKS before each JWT verification.
*/
skipJwksCache?: boolean;
/**
* The Clerk Secret Key from the [**API keys**](https://dashboard.clerk.com/last-active?path=api-keys) page in the Clerk Dashboard.
*/
secretKey?: string;
/**
* The [Clerk Backend API](https://clerk.com/docs/reference/backend-api) endpoint.
* The [Clerk Backend API](https://clerk.com/docs/reference/backend-api){{ target: '_blank' }} endpoint.
* @default 'https://api.clerk.com'
*/
apiUrl?: string;
Expand Down
67 changes: 67 additions & 0 deletions packages/backend/src/tokens/verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { decodeJwt, verifyJwt } from '../jwt/verifyJwt';
import type { LoadClerkJWKFromRemoteOptions } from './keys';
import { loadClerkJWKFromLocal, loadClerkJWKFromRemote } from './keys';

/**
* @interface
*/
export type VerifyTokenOptions = Omit<VerifyJwtOptions, 'key'> &
Omit<LoadClerkJWKFromRemoteOptions, 'kid'> & {
/**
Expand All @@ -15,6 +18,70 @@ export type VerifyTokenOptions = Omit<VerifyJwtOptions, 'key'> &
jwtKey?: string;
};

/**
* > [!WARNING]
* > This is a lower-level method intended for more advanced use-cases. It's recommended to use [`authenticateRequest()`](https://clerk.com/docs/references/backend/authenticate-request), which fully authenticates a token passed from the `request` object.
*
* Verifies a Clerk-generated token signature. Networkless if the `jwtKey` is provided. Otherwise, performs a network call to retrieve the JWKS from the [Backend API](https://clerk.com/docs/reference/backend-api/tag/JWKS#operation/GetJWKS){{ target: '_blank' }}.
*
* @param token - The token to verify.
* @param options - Options for verifying the token.
*
* @example
*
* The following example demonstrates how to use the [JavaScript Backend SDK](https://clerk.com/docs/references/backend/overview) to verify the token signature.
*
* In the following example:
*
* 1. The **JWKS Public Key** from the Clerk Dashboard is set in the environment variable `CLERK_JWT_KEY`.
* 1. The session token is retrieved from the `__session` cookie or the Authorization header.
* 1. The token is verified in a networkless manner by passing the `jwtKey` prop.
* 1. The `authorizedParties` prop is passed to verify that the session token is generated from the expected frontend application.
* 1. If the token is valid, the response contains the verified token.
*
* ```ts
* import { verifyToken } from '@clerk/backend'
* import { cookies } from 'next/headers'
*
* export async function GET(request: Request) {
* const cookieStore = cookies()
* const sessToken = cookieStore.get('__session')?.value
* const bearerToken = request.headers.get('Authorization')?.replace('Bearer ', '')
* const token = sessToken || bearerToken
*
* if (!token) {
* return Response.json({ error: 'Token not found. User must sign in.' }, { status: 401 })
* }
*
* try {
* const verifiedToken = await verifyToken(token, {
* jwtKey: process.env.CLERK_JWT_KEY,
* authorizedParties: ['http://localhost:3001', 'api.example.com'], // Replace with your authorized parties
* })
*
* return Response.json({ verifiedToken })
* } catch (error) {
* return Response.json({ error: 'Token not verified.' }, { status: 401 })
* }
* }
* ```
*
* If the token is valid, the response will contain a JSON object that looks something like this:
*
* ```json
* {
* "verifiedToken": {
* "azp": "http://localhost:3000",
* "exp": 1687906422,
* "iat": 1687906362,
* "iss": "https://magical-marmoset-51.clerk.accounts.dev",
* "nbf": 1687906352,
* "sid": "sess_2Ro7e2IxrffdqBboq8KfB6eGbIy",
* "sub": "user_2RfWKJREkjKbHZy0Wqa5qrHeAnb"
* }
* }
* ```
*/
export async function verifyToken(
token: string,
options: VerifyTokenOptions,
Expand Down
33 changes: 19 additions & 14 deletions packages/backend/src/webhooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { Webhook } from 'svix';

import type { WebhookEvent } from './api/resources/Webhooks';

/**
* @inline
*/
export type VerifyWebhookOptions = {
/**
* The signing secret for the webhook. It's recommended to use the [`CLERK_WEBHOOK_SIGNING_SECRET` environment variable](https://clerk.com/docs/deployments/clerk-environment-variables#webhooks) instead.
*/
signingSecret?: string;
};

Expand All @@ -17,33 +23,32 @@ const REQUIRED_SVIX_HEADERS = [SVIX_ID_HEADER, SVIX_TIMESTAMP_HEADER, SVIX_SIGNA
export * from './api/resources/Webhooks';

/**
* Verifies the authenticity of a webhook request using Svix.
* Verifies the authenticity of a webhook request using Svix. Returns a promise that resolves to the verified webhook event data.
*
* @param request - The incoming webhook request object
* @param options - Optional configuration object
* @param options.signingSecret - Custom signing secret. If not provided, falls back to CLERK_WEBHOOK_SIGNING_SECRET env variable
* @throws Will throw an error if the webhook signature verification fails
* @returns A promise that resolves to the verified webhook event data
* @param request - The request object.
* @param options - Optional configuration object.
*
* @example
* ```typescript
* See the [guide on syncing data](https://clerk.com/docs/webhooks/sync-data) for more comprehensive and framework-specific examples that you can copy and paste into your app.
*
* ```ts
* try {
* const evt = await verifyWebhook(request);
* const evt = await verifyWebhook(request)
*
* // Access the event data
* const { id } = evt.data;
* const eventType = evt.type;
* const { id } = evt.data
* const eventType = evt.type
*
* // Handle specific event types
* if (evt.type === 'user.created') {
* console.log('New user created:', evt.data.id);
* console.log('New user created:', evt.data.id)
* // Handle user creation
* }
*
* return new Response('Success', { status: 200 });
* return new Response('Success', { status: 200 })
* } catch (err) {
* console.error('Webhook verification failed:', err);
* return new Response('Webhook verification failed', { status: 400 });
* console.error('Webhook verification failed:', err)
* return new Response('Webhook verification failed', { status: 400 })
* }
* ```
*/
Expand Down
4 changes: 4 additions & 0 deletions packages/backend/typedoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["./src/index.ts", "./src/webhooks.ts", "./src/tokens/verify.ts"]
}
2 changes: 1 addition & 1 deletion typedoc.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ const config = {
disableSources: true,
...typedocPluginReplaceTextOptions,
},
entryPoints: ['packages/nextjs', 'packages/react', 'packages/shared', 'packages/types'], // getPackages(),
entryPoints: ['packages/backend', 'packages/nextjs', 'packages/react', 'packages/shared', 'packages/types'], // getPackages(),
...typedocPluginMarkdownOptions,
};

Expand Down