+
+[](https://clerk.com/discord)
+[](https://beta.clerk.com/docs?utm_source=github&utm_medium=express)
+[](https://twitter.com/intent/follow?screen_name=ClerkDev)
+
+[Changelog](https://github.com/clerk/javascript/blob/main/packages/express/CHANGELOG.md)
+·
+[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml)
+·
+[Request a Feature](https://feedback.clerk.com/roadmap)
+·
+[Ask a Question](https://github.com/clerk/javascript/discussions)
+
+
+
+---
+
+## Overview
+
+[Clerk](https://clerk.com?utm_source=github&utm_medium=express) is the easiest way to add authentication and user management to your Node.js application. To gain a better understanding of the Express SDK and Clerk Backend API, refer to
+the Express SDK and Backend API documentation.
+
+## Getting started
+
+### Prerequisites
+
+- Node.js `>=18.17.0` or later
+- Express installed (follow their [Getting started](https://expressjs.com/en/starter/installing.html) guide)
+
+## Installation
+
+```sh
+npm install @clerk/express
+```
+
+To build the package locally with the TypeScript compiler, run:
+
+```sh
+npm run build
+```
+
+## Usage
+
+Retrieve your Backend API key from the [API Keys](https://dashboard.clerk.com/last-active?path=api-keys) screen in your Clerk dashboard and set it as an environment variable in a `.env` file:
+
+```sh
+CLERK_PUBLISHABLE_KEY=pk_*******
+CLERK_SECRET_KEY=sk_******
+```
+
+You will then be able to access all the available methods.
+
+```js
+import 'dotenv/config'; // To read CLERK_SECRET_KEY
+import { clerkClient } from '@clerk/express';
+
+const { data: userList } = await clerkClient.users.getUserList();
+```
+
+_For further details and examples, please refer to our [Documentation](https://beta.clerk.com/docs/references/express/overview?utm_source=github&utm_medium=express)._
+
+## Support
+
+You can get in touch with us in any of the following ways:
+
+- Join our official community [Discord server](https://clerk.com/discord)
+- Create a [GitHub Discussion](https://github.com/clerk/javascript/discussions)
+- Contact options listed on [our Support page](https://clerk.com/support?utm_source=github&utm_medium=express)
+
+## Contributing
+
+We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md).
+
+## Security
+
+`@clerk/express` follows good practices of security, but 100% security cannot be assured.
+
+`@clerk/express` is provided **"as is"** without any **warranty**. Use at your own risk.
+
+_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._
+
+## License
+
+This project is licensed under the **MIT license**.
+
+See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/express/LICENSE) for more information.
diff --git a/packages/express/jest.config.js b/packages/express/jest.config.js
new file mode 100644
index 00000000000..71b21269826
--- /dev/null
+++ b/packages/express/jest.config.js
@@ -0,0 +1,18 @@
+const { name } = require('./package.json');
+
+module.exports = {
+ displayName: name.replace('@clerk', ''),
+ globals: {
+ PACKAGE_NAME: '@clerk/express',
+ PACKAGE_VERSION: '0.0.0-test',
+ },
+ roots: ['/src'],
+ testMatch: ['**/?(*.)+(test).+(ts)'],
+ transform: {
+ '^.+\\.ts$': 'ts-jest',
+ },
+ setupFiles: ['/jest.setup.js'],
+ collectCoverage: true,
+ coveragePathIgnorePatterns: ['__tests__'],
+ testPathIgnorePatterns: ['/node_modules/', '/coverage', '/dist/'],
+};
diff --git a/packages/express/jest.setup.js b/packages/express/jest.setup.js
new file mode 100644
index 00000000000..cf6bc7526d0
--- /dev/null
+++ b/packages/express/jest.setup.js
@@ -0,0 +1,2 @@
+process.env.CLERK_SECRET_KEY = 'sk_test_....';
+process.env.CLERK_PUBLISHABLE_KEY = 'pk_test_Y2xlcmsuaW5jbHVkZWQua2F0eWRpZC05Mi5sY2wuZGV2JA';
diff --git a/packages/express/package.json b/packages/express/package.json
new file mode 100644
index 00000000000..4d41f76d2eb
--- /dev/null
+++ b/packages/express/package.json
@@ -0,0 +1,77 @@
+{
+ "name": "@clerk/express",
+ "version": "1.0.0",
+ "description": "Clerk server SDK for usage with Express",
+ "keywords": [
+ "clerk",
+ "sdk",
+ "express"
+ ],
+ "homepage": "https://clerk.com/",
+ "bugs": {
+ "url": "https://github.com/clerk/javascript/issues"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/clerk/javascript.git",
+ "directory": "packages/express"
+ },
+ "license": "MIT",
+ "author": {
+ "name": "Clerk, Inc.",
+ "email": "support@clerk.com",
+ "url": "git+https://github.com/clerk/javascript.git"
+ },
+ "exports": {
+ ".": {
+ "import": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "require": {
+ "types": "./dist/index.d.ts",
+ "default": "./dist/index.js"
+ }
+ },
+ "./package.json": "./package.json"
+ },
+ "main": "./dist/index.js",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "npm run clean && tsup",
+ "clean": "rimraf ./dist",
+ "dev": "tsup --watch",
+ "dev:publish": "npm run dev -- --env.publish",
+ "lint": "eslint src/",
+ "lint:attw": "attw --pack .",
+ "lint:publint": "publint",
+ "publish:local": "npx yalc push --replace --sig",
+ "test": "jest",
+ "test:cache:clear": "jest --clearCache --useStderr",
+ "test:ci": "jest --maxWorkers=70%"
+ },
+ "dependencies": {
+ "@clerk/backend": "^1.0.0-beta.28",
+ "@clerk/shared": "^2.0.0-beta.19",
+ "tslib": "2.4.1"
+ },
+ "devDependencies": {
+ "@clerk/types": "4.0.0-beta.18",
+ "@types/express": "^4.17",
+ "@types/node": "^18.17.0",
+ "@types/supertest": "^6.0.2",
+ "eslint-config-custom": "*",
+ "express": "^4.19.2",
+ "supertest": "^6.3.4",
+ "tsup": "*",
+ "typescript": "*"
+ },
+ "engines": {
+ "node": ">=18.17.0"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/express/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/express/src/__tests__/__snapshots__/exports.test.ts.snap
new file mode 100644
index 00000000000..406f539783d
--- /dev/null
+++ b/packages/express/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -0,0 +1,12 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`module exports should not change unless explicitly set 1`] = `
+[
+ "clerkClient",
+ "clerkMiddleware",
+ "createClerkClient",
+ "getAuth",
+ "requireAuth",
+ "verifyToken",
+]
+`;
diff --git a/packages/express/src/__tests__/clerkMiddleware.test.ts b/packages/express/src/__tests__/clerkMiddleware.test.ts
new file mode 100644
index 00000000000..39f39f56ca8
--- /dev/null
+++ b/packages/express/src/__tests__/clerkMiddleware.test.ts
@@ -0,0 +1,138 @@
+import type { RequestHandler } from 'express';
+
+import { clerkMiddleware } from '../clerkMiddleware';
+import { getAuth } from '../getAuth';
+import { requireAuth } from '../requireAuth';
+import { assertNoDebugHeaders, assertSignedOutDebugHeaders, runMiddleware } from './helpers';
+
+describe('clerkMiddleware', () => {
+ // TODO(dimkl): Fix issue that makes test order matter, until then keep this test suite first
+ describe('without secretKey env', () => {
+ const sk = process.env.CLERK_SECRET_KEY;
+
+ beforeEach(() => {
+ delete process.env.CLERK_SECRET_KEY;
+ });
+
+ afterEach(() => {
+ process.env.CLERK_SECRET_KEY = sk;
+ });
+
+ it('throws error if secretKey is not passed as parameter', async () => {
+ const response = await runMiddleware(clerkMiddleware()).expect(500);
+
+ assertNoDebugHeaders(response);
+ });
+
+ it('works if secretKey is passed as parameter', async () => {
+ const options = { secretKey: 'sk_test_....' };
+
+ const response = await runMiddleware(clerkMiddleware(options)).expect(200, 'Hello world!');
+
+ assertSignedOutDebugHeaders(response);
+ });
+ });
+
+ // TODO(dimkl): Fix issue that makes test order matter, until then keep this test suite second
+ describe('without publishableKey env', () => {
+ const pk = process.env.CLERK_PUBLISHABLE_KEY;
+
+ beforeEach(() => {
+ delete process.env.CLERK_PUBLISHABLE_KEY;
+ });
+
+ afterEach(() => {
+ process.env.CLERK_PUBLISHABLE_KEY = pk;
+ });
+
+ it('throws error if publishableKey is not passed as parameter', async () => {
+ const response = await runMiddleware(clerkMiddleware()).expect(500);
+
+ assertNoDebugHeaders(response);
+ });
+
+ it('works if publishableKey is passed as parameter', async () => {
+ const options = { publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' };
+
+ const response = await runMiddleware(clerkMiddleware(options)).expect(200, 'Hello world!');
+
+ assertSignedOutDebugHeaders(response);
+ });
+ });
+
+ it.todo('supports usage without invocation: app.use(clerkMiddleware)');
+
+ it('supports usage without parameters: app.use(clerkMiddleware())', async () => {
+ const response = await runMiddleware(clerkMiddleware()).expect(200, 'Hello world!');
+
+ assertSignedOutDebugHeaders(response);
+ });
+
+ it('supports usage with parameters: app.use(clerkMiddleware(options))', async () => {
+ const options = { publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' };
+
+ const response = await runMiddleware(clerkMiddleware(options)).expect(200, 'Hello world!');
+
+ assertSignedOutDebugHeaders(response);
+ });
+
+ it('supports usage with request handler: app.use(clerkMiddleware(requestHandler))', async () => {
+ const handler: RequestHandler = (_req, res, next) => {
+ res.setHeader('x-clerk-auth-custom', 'custom-value');
+ return next();
+ };
+
+ const response = await runMiddleware(clerkMiddleware(handler)).expect(200, 'Hello world!');
+
+ expect(response.header).toHaveProperty('x-clerk-auth-custom', 'custom-value');
+ assertSignedOutDebugHeaders(response);
+ });
+
+ it('supports usage with parameters and request handler: app.use(clerkMiddleware(requestHandler, options))', async () => {
+ const handler: RequestHandler = (_req, res, next) => {
+ res.setHeader('x-clerk-auth-custom', 'custom-value');
+ return next();
+ };
+ const options = { publishableKey: 'pk_test_Y2xlcmsuZXhhbXBsZS5jb20k' };
+
+ const response = await runMiddleware(clerkMiddleware(handler, options)).expect(200, 'Hello world!');
+
+ expect(response.header).toHaveProperty('x-clerk-auth-custom', 'custom-value');
+ assertSignedOutDebugHeaders(response);
+ });
+
+ it('throws error if clerkMiddleware is not executed before requireAuth', async () => {
+ const customMiddleware: RequestHandler = (_request, response, next) => {
+ response.setHeader('x-custom-middleware', 'custom');
+ return next();
+ };
+
+ const response = await runMiddleware([requireAuth, customMiddleware]).expect(500);
+
+ assertNoDebugHeaders(response);
+ expect(response.header).not.toHaveProperty('x-clerk-auth-custom', 'custom-value');
+ });
+
+ it('throws error if clerkMiddleware is not executed before getAuth', async () => {
+ const customMiddleware: RequestHandler = (request, response, next) => {
+ const auth = getAuth(request);
+ response.setHeader('x-custom-middleware', auth.userId || '');
+ return next();
+ };
+
+ const response = await runMiddleware([customMiddleware]).expect(500);
+
+ assertNoDebugHeaders(response);
+ expect(response.header).not.toHaveProperty('x-clerk-auth-custom', 'custom-value');
+ });
+
+ it('supports handshake flow', async () => {
+ const response = await runMiddleware(clerkMiddleware(), {
+ Cookie: '__client_uat=1711618859;',
+ 'Sec-Fetch-Dest': 'document',
+ }).expect(307);
+
+ expect(response.header).toHaveProperty('x-clerk-auth-status', 'handshake');
+ expect(response.header).toHaveProperty('location', expect.stringContaining('/v1/client/handshake?redirect_url='));
+ });
+});
diff --git a/packages/express/src/__tests__/exports.test.ts b/packages/express/src/__tests__/exports.test.ts
new file mode 100644
index 00000000000..632b0e0fcbf
--- /dev/null
+++ b/packages/express/src/__tests__/exports.test.ts
@@ -0,0 +1,7 @@
+import * as publicExports from '..';
+
+describe('module exports', () => {
+ it('should not change unless explicitly set', () => {
+ expect(Object.keys(publicExports).sort()).toMatchSnapshot();
+ });
+});
diff --git a/packages/express/src/__tests__/getAuth.test.ts b/packages/express/src/__tests__/getAuth.test.ts
new file mode 100644
index 00000000000..eaa3e6337ba
--- /dev/null
+++ b/packages/express/src/__tests__/getAuth.test.ts
@@ -0,0 +1,17 @@
+import { getAuth } from '../getAuth';
+import { mockRequest, mockRequestWithAuth } from './helpers';
+
+describe('getAuth', () => {
+ it('throws error if clerkMiddleware is not executed before getAuth', async () => {
+ expect(() => getAuth(mockRequest())).toThrow(/The "clerkMiddleware" should be registered before using "getAuth"/);
+ });
+
+ it('returns auth from request for signed-out request', async () => {
+ expect(getAuth(mockRequestWithAuth())).toHaveProperty('userId', null);
+ });
+
+ it('returns auth from request', async () => {
+ const req = mockRequestWithAuth({ userId: 'user_12345' });
+ expect(getAuth(req)).toHaveProperty('userId', 'user_12345');
+ });
+});
diff --git a/packages/express/src/__tests__/helpers.ts b/packages/express/src/__tests__/helpers.ts
new file mode 100644
index 00000000000..45a6480b95f
--- /dev/null
+++ b/packages/express/src/__tests__/helpers.ts
@@ -0,0 +1,57 @@
+import type { AuthObject } from '@clerk/backend/internal';
+import type { Application, Request as ExpressRequest, RequestHandler, Response as ExpressResponse } from 'express';
+import express from 'express';
+import supertest from 'supertest';
+
+import type { ExpressRequestWithAuth } from '../types';
+
+// Inspired by https://github.com/helmetjs/helmet/blob/main/test/helpers.ts
+export function runMiddleware(middleware: RequestHandler | RequestHandler[], headers: Record = {}) {
+ const app: Application = express();
+ app.use(middleware);
+ app.use((_req, res, _next) => res.end('Hello world!'));
+
+ return supertest(app).get('/').set(headers);
+}
+
+export function mockResponse(): ExpressResponse {
+ return {
+ status: jest.fn().mockReturnThis(),
+ send: jest.fn().mockReturnThis(),
+ } as unknown as ExpressResponse;
+}
+
+export function mockRequest(): ExpressRequest {
+ return {} as ExpressRequest;
+}
+
+export function mockRequestWithAuth(auth: Partial = {}): ExpressRequestWithAuth {
+ return {
+ auth: {
+ sessionClaims: null,
+ sessionId: null,
+ actor: null,
+ userId: null,
+ orgId: null,
+ orgRole: null,
+ orgSlug: null,
+ orgPermissions: null,
+ getToken: async () => '',
+ has: () => false,
+ debug: () => ({}),
+ ...auth,
+ },
+ } as unknown as ExpressRequestWithAuth;
+}
+
+export function assertSignedOutDebugHeaders(response: any) {
+ expect(response.header).toHaveProperty('x-clerk-auth-status', 'signed-out');
+ expect(response.header).toHaveProperty('x-clerk-auth-reason', 'session-token-and-uat-missing');
+ expect(response.header).not.toHaveProperty('x-clerk-auth-message');
+}
+
+export function assertNoDebugHeaders(response: any) {
+ expect(response.header).not.toHaveProperty('x-clerk-auth-status');
+ expect(response.header).not.toHaveProperty('x-clerk-auth-reason');
+ expect(response.header).not.toHaveProperty('x-clerk-auth-message');
+}
diff --git a/packages/express/src/__tests__/requireAuth.test.ts b/packages/express/src/__tests__/requireAuth.test.ts
new file mode 100644
index 00000000000..afc554c5d25
--- /dev/null
+++ b/packages/express/src/__tests__/requireAuth.test.ts
@@ -0,0 +1,31 @@
+import { requireAuth } from '../requireAuth';
+import { mockRequest, mockRequestWithAuth, mockResponse } from './helpers';
+
+describe('requireAuth', () => {
+ it('throws error if clerkMiddleware is not executed before this middleware', async () => {
+ expect(() => requireAuth(mockRequest(), mockResponse(), () => undefined)).toThrow(
+ /The "clerkMiddleware" should be registered before using "requireAuth"/,
+ );
+ });
+
+ it('make application require auth - returns 401 Unauthorized for signed-out', async () => {
+ const response = mockResponse();
+ const nextFn = jest.fn();
+
+ requireAuth(mockRequestWithAuth(), response, nextFn);
+
+ expect(response.status).toBeCalledWith(401);
+ expect(nextFn).not.toBeCalled();
+ });
+
+ it('make application require auth - proceed with next middlewares for signed-in', async () => {
+ const response = mockResponse();
+ const nextFn = jest.fn();
+ const request = mockRequestWithAuth({ userId: 'user_1234' });
+
+ requireAuth(request, response, nextFn);
+
+ expect(response.status).not.toBeCalled();
+ expect(nextFn).toBeCalled();
+ });
+});
diff --git a/packages/express/src/authenticateRequest.ts b/packages/express/src/authenticateRequest.ts
new file mode 100644
index 00000000000..57849e6630b
--- /dev/null
+++ b/packages/express/src/authenticateRequest.ts
@@ -0,0 +1,97 @@
+import type { RequestState } from '@clerk/backend/internal';
+import { AuthStatus, createClerkRequest } from '@clerk/backend/internal';
+import { handleValueOrFn } from '@clerk/shared/handleValueOrFn';
+import { isDevelopmentFromSecretKey } from '@clerk/shared/keys';
+import { isHttpOrHttps, isProxyUrlRelative, isValidProxyUrl } from '@clerk/shared/proxy';
+import type { Response } from 'express';
+import type { IncomingMessage } from 'http';
+
+import { satelliteAndMissingProxyUrlAndDomain, satelliteAndMissingSignInUrl } from './errors';
+import type { AuthenticateRequestParams } from './types';
+import { loadApiEnv, loadClientEnv } from './utils';
+
+export const authenticateRequest = (opts: AuthenticateRequestParams) => {
+ const { clerkClient, request, options } = opts;
+ const { jwtKey, authorizedParties, audience } = options || {};
+
+ const clerkRequest = createClerkRequest(incomingMessageToRequest(request));
+ const env = { ...loadApiEnv(), ...loadClientEnv() };
+
+ const secretKey = options?.secretKey || env.secretKey;
+ const publishableKey = options?.publishableKey || env.publishableKey;
+
+ const isSatellite = handleValueOrFn(options?.isSatellite, clerkRequest.clerkUrl, env.isSatellite);
+ const domain = handleValueOrFn(options?.domain, clerkRequest.clerkUrl) || env.domain;
+ const signInUrl = options?.signInUrl || env.signInUrl;
+ const proxyUrl = absoluteProxyUrl(
+ handleValueOrFn(options?.proxyUrl, clerkRequest.clerkUrl, env.proxyUrl),
+ clerkRequest.clerkUrl.toString(),
+ );
+
+ if (isSatellite && !proxyUrl && !domain) {
+ throw new Error(satelliteAndMissingProxyUrlAndDomain);
+ }
+
+ if (isSatellite && !isHttpOrHttps(signInUrl) && isDevelopmentFromSecretKey(secretKey || '')) {
+ throw new Error(satelliteAndMissingSignInUrl);
+ }
+
+ return clerkClient.authenticateRequest(clerkRequest, {
+ audience,
+ secretKey,
+ publishableKey,
+ jwtKey,
+ authorizedParties,
+ proxyUrl,
+ isSatellite,
+ domain,
+ signInUrl,
+ });
+};
+
+const incomingMessageToRequest = (req: IncomingMessage): Request => {
+ const headers = Object.keys(req.headers).reduce((acc, key) => Object.assign(acc, { [key]: req?.headers[key] }), {});
+ // @ts-ignore Optimistic attempt to get the protocol in case
+ // req extends IncomingMessage in a useful way. No guarantee
+ // it'll work.
+ const protocol = req.connection?.encrypted ? 'https' : 'http';
+ const dummyOriginReqUrl = new URL(req.url || '', `${protocol}://clerk-dummy`);
+ return new Request(dummyOriginReqUrl, {
+ method: req.method,
+ headers: new Headers(headers),
+ });
+};
+
+export const setResponseHeaders = (requestState: RequestState, res: Response): Error | undefined => {
+ if (requestState.headers) {
+ requestState.headers.forEach((value, key) => res.appendHeader(key, value));
+ }
+ return setResponseForHandshake(requestState, res);
+};
+
+/**
+ * Depending on the auth state of the request, handles applying redirects and validating that a handshake state was properly handled.
+ *
+ * Returns an error if state is handshake without a redirect, otherwise returns undefined. res.writableEnded should be checked after this method is called.
+ */
+const setResponseForHandshake = (requestState: RequestState, res: Response): Error | undefined => {
+ const hasLocationHeader = requestState.headers.get('location');
+ if (hasLocationHeader) {
+ // triggering a handshake redirect
+ res.status(307).end();
+ return;
+ }
+
+ if (requestState.status === AuthStatus.Handshake) {
+ return new Error('Clerk: unexpected handshake without redirect');
+ }
+
+ return;
+};
+
+const absoluteProxyUrl = (relativeOrAbsoluteUrl: string, baseUrl: string): string => {
+ if (!relativeOrAbsoluteUrl || !isValidProxyUrl(relativeOrAbsoluteUrl) || !isProxyUrlRelative(relativeOrAbsoluteUrl)) {
+ return relativeOrAbsoluteUrl;
+ }
+ return new URL(relativeOrAbsoluteUrl, baseUrl).toString();
+};
diff --git a/packages/express/src/clerkClient.ts b/packages/express/src/clerkClient.ts
new file mode 100644
index 00000000000..eb8ee20a512
--- /dev/null
+++ b/packages/express/src/clerkClient.ts
@@ -0,0 +1,28 @@
+import type { ClerkClient } from '@clerk/backend';
+import { createClerkClient } from '@clerk/backend';
+
+import { loadApiEnv, loadClientEnv } from './utils';
+
+let clerkClientSingleton = {} as unknown as ClerkClient;
+
+export const clerkClient = new Proxy(clerkClientSingleton, {
+ get(_target, property: keyof ClerkClient) {
+ if (property in clerkClientSingleton) {
+ return clerkClientSingleton[property];
+ }
+
+ const env = { ...loadApiEnv(), ...loadClientEnv() };
+ const client = createClerkClient({ ...env, userAgent: `${PACKAGE_NAME}@${PACKAGE_VERSION}` });
+
+ // if the client is initialized properly, cache it to a singleton instance variable
+ // in the next invocation the guard at the top will be triggered instead of creating another instance
+ if (env.secretKey) {
+ clerkClientSingleton = client;
+ }
+
+ return client[property];
+ },
+ set() {
+ return false;
+ },
+});
diff --git a/packages/express/src/clerkMiddleware.ts b/packages/express/src/clerkMiddleware.ts
new file mode 100644
index 00000000000..de622a3f7ac
--- /dev/null
+++ b/packages/express/src/clerkMiddleware.ts
@@ -0,0 +1,57 @@
+import type { RequestHandler } from 'express';
+
+import { authenticateRequest, setResponseHeaders } from './authenticateRequest';
+import { clerkClient as defaultClerkClient } from './clerkClient';
+import { middlewareNotInvoked } from './errors';
+import type { ClerkMiddleware, ClerkMiddlewareOptions } from './types';
+import { defaultHandler } from './utils';
+
+const usedWithoutInvocation = (args: unknown[]) => {
+ return (
+ args.length === 3 && typeof args[0] === 'object' && typeof args[1] === 'object' && typeof args[2] === 'function'
+ );
+};
+
+const parseHandlerAndOptions = (args: unknown[]) => {
+ return [
+ typeof args[0] === 'function' ? args[0] : undefined,
+ (args.length === 2 ? args[1] : typeof args[0] === 'function' ? {} : args[0]) || {},
+ ] as [RequestHandler | undefined, ClerkMiddlewareOptions];
+};
+
+export const clerkMiddleware: ClerkMiddleware = (...args: unknown[]) => {
+ const [handler, options] = parseHandlerAndOptions(args);
+
+ const clerkClient = options.clerkClient || defaultClerkClient;
+
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
+ const middleware: RequestHandler = async (request, response, next) => {
+ try {
+ const requestState = await authenticateRequest({
+ clerkClient,
+ request,
+ options,
+ });
+
+ const err = setResponseHeaders(requestState, response);
+ if (err || response.writableEnded) {
+ if (err) {
+ next(err);
+ }
+ return;
+ }
+
+ Object.assign(request, { auth: requestState.toAuth() });
+
+ return next();
+ } catch (err) {
+ next(err);
+ }
+ };
+
+ if (usedWithoutInvocation(args)) {
+ throw new Error(middlewareNotInvoked);
+ }
+
+ return [middleware, handler || defaultHandler];
+};
diff --git a/packages/express/src/errors.ts b/packages/express/src/errors.ts
new file mode 100644
index 00000000000..0bbb461990e
--- /dev/null
+++ b/packages/express/src/errors.ts
@@ -0,0 +1,28 @@
+const createErrorMessage = (msg: string) => {
+ return `🔒 Clerk: ${msg.trim()}
+
+ For more info, check out the docs: https://clerk.com/docs,
+ or come say hi in our discord server: https://clerk.com/discord
+ `;
+};
+
+export const middlewareRequired = (fnName: string) =>
+ createErrorMessage(`The "clerkMiddleware" should be registered before using "${fnName}".
+Example:
+
+import express from 'express';
+import { clerkMiddleware } from '@clerk/express';
+
+const app = express();
+app.use(clerkMiddleware());
+`);
+
+export const middlewareNotInvoked = createErrorMessage(
+ `The "clerkMiddleware" should be invoked. Use "clerkMiddleware()"`,
+);
+
+export const satelliteAndMissingProxyUrlAndDomain =
+ 'Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl';
+export const satelliteAndMissingSignInUrl = `
+Invalid signInUrl. A satellite application requires a signInUrl for development instances.
+Check if signInUrl is missing from your configuration or if it is not an absolute URL.`;
diff --git a/packages/express/src/getAuth.ts b/packages/express/src/getAuth.ts
new file mode 100644
index 00000000000..41a9b0e0c7d
--- /dev/null
+++ b/packages/express/src/getAuth.ts
@@ -0,0 +1,20 @@
+import type { AuthObject } from '@clerk/backend/internal';
+import type { Request as ExpressRequest } from 'express';
+
+import { middlewareRequired } from './errors';
+import { requestHasAuthObject } from './utils';
+
+/**
+ * Retrieves the Clerk AuthObject using the current request object.
+ *
+ * @param {ExpressRequest} req - The current request object.
+ * @returns {AuthObject} Object with information about the request state and claims.
+ * @throws {Error} `clerkMiddleware` is required to be set in the middleware chain before this util is used.
+ */
+export const getAuth = (req: ExpressRequest): AuthObject => {
+ if (!requestHasAuthObject(req)) {
+ throw new Error(middlewareRequired('getAuth'));
+ }
+
+ return req.auth;
+};
diff --git a/packages/express/src/globals.d.ts b/packages/express/src/globals.d.ts
new file mode 100644
index 00000000000..f14f854fd7d
--- /dev/null
+++ b/packages/express/src/globals.d.ts
@@ -0,0 +1,6 @@
+export {};
+
+declare global {
+ const PACKAGE_NAME: string;
+ const PACKAGE_VERSION: string;
+}
diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts
new file mode 100644
index 00000000000..99e44657280
--- /dev/null
+++ b/packages/express/src/index.ts
@@ -0,0 +1,8 @@
+export * from '@clerk/backend';
+
+export { clerkClient } from './clerkClient';
+
+export type { ClerkMiddleware, ExpressRequestWithAuth } from './types';
+export { clerkMiddleware } from './clerkMiddleware';
+export { getAuth } from './getAuth';
+export { requireAuth } from './requireAuth';
diff --git a/packages/express/src/requireAuth.ts b/packages/express/src/requireAuth.ts
new file mode 100644
index 00000000000..3aacb20aeb6
--- /dev/null
+++ b/packages/express/src/requireAuth.ts
@@ -0,0 +1,39 @@
+import type { RequestHandler } from 'express';
+
+import { middlewareRequired } from './errors';
+import { getAuth } from './getAuth';
+import { requestHasAuthObject } from './utils';
+
+/**
+ * Middleware to require auth requests for user authenticated or authorized requests.
+ * An HTTP 401 status code is returned for unauthenticated requests.
+ *
+ * @example
+ * router.get('/path', requireAuth, getHandler)
+ * //or
+ * router.use(requireAuth)
+ * @example
+ * hasPermission = (request, response, next) => {
+ * const auth = getAuth(request);
+ * if (!auth.has({ permission: 'permission' })) {
+ * response.status(403).send('Forbidden');
+ * return;
+ * }
+ * return next();
+ * }
+ * router.get('/path', requireAuth, hasPermission, getHandler)
+ *
+ * @throws {Error} `clerkMiddleware` is required to be set in the middleware chain before this util is used.
+ */
+export const requireAuth: RequestHandler = (request, response, next) => {
+ if (!requestHasAuthObject(request)) {
+ throw new Error(middlewareRequired('requireAuth'));
+ }
+
+ if (!getAuth(request).userId) {
+ response.status(401).send('Unauthorized');
+ return;
+ }
+
+ return next();
+};
diff --git a/packages/express/src/types.ts b/packages/express/src/types.ts
new file mode 100644
index 00000000000..afe420298ed
--- /dev/null
+++ b/packages/express/src/types.ts
@@ -0,0 +1,47 @@
+// eslint-disable-next-line @typescript-eslint/consistent-type-imports
+import { createClerkClient } from '@clerk/backend';
+import type { AuthenticateRequestOptions, AuthObject } from '@clerk/backend/internal';
+import type { Request as ExpressRequest, RequestHandler } from 'express';
+
+export type ExpressRequestWithAuth = ExpressRequest & { auth: AuthObject };
+
+export type ClerkMiddlewareOptions = AuthenticateRequestOptions & {
+ debug?: boolean;
+ clerkClient?: ClerkClient;
+};
+
+/**
+ * Middleware for Express that handles authentication and authorization with Clerk.
+ * For more details, please refer to the docs: https://beta.clerk.com/docs/references/express/overview?utm_source=github&utm_medium=expres
+ */
+export interface ClerkMiddleware {
+ /**
+ * @example
+ * const handler = (request, response, next) => {
+ * ...;
+ * // if next is not called the request will be terminated or hung.
+ * return next();
+ * }
+ * app.use(clerkMiddleware(handler, options));
+ */
+ (handler: RequestHandler, options?: ClerkMiddlewareOptions): RequestHandler[];
+ /**
+ * @example
+ * app.use(clerkMiddleware(options));
+ *
+ * @example
+ * const clerkClient = createClerkClient({ ... });
+ * app.use(clerkMiddleware({ clerkClient }));
+ *
+ * @example
+ * app.use(clerkMiddleware());
+ */
+ (options?: ClerkMiddlewareOptions): RequestHandler[];
+}
+
+type ClerkClient = ReturnType;
+export type AuthenticateRequestParams = {
+ clerkClient: ClerkClient;
+ request: ExpressRequest;
+ options?: ClerkMiddlewareOptions;
+};
diff --git a/packages/express/src/utils.ts b/packages/express/src/utils.ts
new file mode 100644
index 00000000000..bc045f952f4
--- /dev/null
+++ b/packages/express/src/utils.ts
@@ -0,0 +1,36 @@
+import { isTruthy } from '@clerk/shared/underscore';
+import type { Request as ExpressRequest, RequestHandler } from 'express';
+
+import type { ExpressRequestWithAuth } from './types';
+
+export const requestHasAuthObject = (req: ExpressRequest): req is ExpressRequestWithAuth => {
+ return 'auth' in req;
+};
+
+export const loadClientEnv = () => {
+ return {
+ publishableKey: process.env.CLERK_PUBLISHABLE_KEY || '',
+ clerkJSUrl: process.env.CLERK_JS || '',
+ clerkJSVersion: process.env.CLERK_JS_VERSION || '',
+ };
+};
+
+export const loadApiEnv = () => {
+ return {
+ secretKey: process.env.CLERK_SECRET_KEY || '',
+ apiUrl: process.env.CLERK_API_URL || 'https://api.clerk.com',
+ apiVersion: process.env.CLERK_API_VERSION || 'v1',
+ domain: process.env.CLERK_DOMAIN || '',
+ proxyUrl: process.env.CLERK_PROXY_URL || '',
+ signInUrl: process.env.CLERK_SIGN_IN_URL || '',
+ isSatellite: isTruthy(process.env.CLERK_IS_SATELLITE),
+ jwtKey: process.env.CLERK_JWT_KEY || '',
+ sdkMetadata: {
+ name: PACKAGE_NAME,
+ version: PACKAGE_VERSION,
+ environment: process.env.NODE_ENV,
+ },
+ };
+};
+
+export const defaultHandler: RequestHandler = (_req, _res, next) => next();
diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json
new file mode 100644
index 00000000000..0f6fe206f62
--- /dev/null
+++ b/packages/express/tsconfig.json
@@ -0,0 +1,23 @@
+{
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": false,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "importHelpers": true,
+ "isolatedModules": true,
+ "moduleResolution": "NodeNext",
+ "module": "NodeNext",
+ "noImplicitReturns": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "resolveJsonModule": true,
+ "sourceMap": false,
+ "strict": true,
+ "target": "ES2020",
+ "outDir": "dist",
+ "types": ["jest"]
+ },
+ "exclude": ["node_modules"],
+ "include": ["src/index.ts", "src/globals.d.ts"]
+}
diff --git a/packages/express/tsconfig.lint.json b/packages/express/tsconfig.lint.json
new file mode 100644
index 00000000000..9b624091911
--- /dev/null
+++ b/packages/express/tsconfig.lint.json
@@ -0,0 +1,5 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src"],
+ "exclude": ["node_modules"]
+}
diff --git a/packages/express/tsup.config.ts b/packages/express/tsup.config.ts
new file mode 100644
index 00000000000..f402b39bbd5
--- /dev/null
+++ b/packages/express/tsup.config.ts
@@ -0,0 +1,22 @@
+import { defineConfig } from 'tsup';
+
+import { name, version } from './package.json';
+
+export default defineConfig(overrideOptions => {
+ const isWatch = !!overrideOptions.watch;
+
+ return {
+ entry: ['./src/index.ts'],
+ format: ['cjs', 'esm'],
+ bundle: true,
+ clean: true,
+ minify: false,
+ sourcemap: true,
+ dts: true,
+ define: {
+ PACKAGE_NAME: `"${name}"`,
+ PACKAGE_VERSION: `"${version}"`,
+ __DEV__: `${isWatch}`,
+ },
+ };
+});
diff --git a/packages/express/turbo.json b/packages/express/turbo.json
new file mode 100644
index 00000000000..258eb4d8b2f
--- /dev/null
+++ b/packages/express/turbo.json
@@ -0,0 +1,19 @@
+{
+ "extends": ["//"],
+ "pipeline": {
+ "build": {
+ "inputs": [
+ "*.d.ts",
+ "**/package.json",
+ "src/**",
+ "tsconfig.json",
+ "tsup.config.ts",
+
+ "!**/__tests__/**",
+ "!**/__snapshots__/**",
+ "!coverage/**",
+ "!node_modules/**"
+ ]
+ }
+ }
+}
diff --git a/packages/sdk-node/package.json b/packages/sdk-node/package.json
index ce726c1f54a..05b207ac3a7 100644
--- a/packages/sdk-node/package.json
+++ b/packages/sdk-node/package.json
@@ -55,8 +55,6 @@
"dependencies": {
"@clerk/backend": "1.0.0-beta.32",
"@clerk/shared": "2.0.0-beta.21",
- "camelcase-keys": "6.2.2",
- "snakecase-keys": "3.2.1",
"tslib": "2.4.1"
},
"devDependencies": {
diff --git a/packages/sdk-node/src/index.ts b/packages/sdk-node/src/index.ts
index 7aa1b40e041..6de071dd83c 100644
--- a/packages/sdk-node/src/index.ts
+++ b/packages/sdk-node/src/index.ts
@@ -12,7 +12,7 @@ import type {
export * from '@clerk/backend';
/**
- * The order of these exports is important, as we want Clerk from clerk/sdk-node
+ * The order of these exports is important, as we want Clerk from clerk/clerk-sdk-node
* to shadow the Clerk export from clerk/backend, because it needs to support
* 2 additional apis: clerk.expressWithAuth, clerk.expressRequireAuth
*/
diff --git a/playground/express/package.json b/playground/express/package.json
index cfe498c0dcc..4fd175d0623 100644
--- a/playground/express/package.json
+++ b/playground/express/package.json
@@ -4,14 +4,14 @@
"private": true,
"scripts": {
"start": "ts-node ./src/server.ts",
- "yalc:add": "yalc add -- @clerk/types @clerk/backend @clerk/clerk-sdk-node",
+ "yalc:add": "yalc add -- @clerk/express @clerk/types @clerk/backend @clerk/shared",
"dev:fromlocal": " nodemon --watch .yalc --watch src --exec \"npm run yalc:add && npm run start\""
},
"author": "",
"license": "ISC",
"dependencies": {
"@clerk/backend": "file:.yalc/@clerk/backend",
- "@clerk/clerk-sdk-node": "file:.yalc/@clerk/clerk-sdk-node",
+ "@clerk/express": "file:.yalc/@clerk/express",
"@clerk/shared": "file:.yalc/@clerk/shared",
"@clerk/types": "file:.yalc/@clerk/types",
"dotenv": "^16.0.3",
@@ -26,4 +26,4 @@
"tslib": "^2.5.0",
"typescript": "4.8.4"
}
-}
\ No newline at end of file
+}
diff --git a/playground/express/src/routes/private.ts b/playground/express/src/routes/private.ts
index 9dcbdff9f47..a76c7d91193 100644
--- a/playground/express/src/routes/private.ts
+++ b/playground/express/src/routes/private.ts
@@ -1,22 +1,23 @@
import type { Response } from 'express';
-import { clerkClient } from '@clerk/clerk-sdk-node';
-import { Router } from 'express';
+import { getAuth, requireAuth } from '@clerk/express';
+import { Router, Request as ExpressRequest } from 'express';
const router = Router();
-router.use((...args)=>clerkClient.expressRequireAuth()(...args));
+router.use(requireAuth);
-router.get('/me', async (req, reply: Response) => {
- return reply.json({ auth: req.auth });
+router.get('/me', async (req: ExpressRequest, reply: Response) => {
+ return reply.json({ auth: getAuth(req) });
});
router.get('/private', async (req, reply: Response) => {
- if (!req.auth.userId) {
+ const auth = getAuth(req);
+ if (!auth.userId) {
return reply.status(403).end();
}
- return reply.json({ hello: 'world', auth: req.auth });
+ return reply.json({ hello: 'world', auth });
});
export const privateRoutes = router;
diff --git a/playground/express/src/routes/public.ts b/playground/express/src/routes/public.ts
index 47797d9a747..fe4c62486ac 100644
--- a/playground/express/src/routes/public.ts
+++ b/playground/express/src/routes/public.ts
@@ -1,4 +1,4 @@
-import { Router } from 'express';
+import { Router, RequestHandler} from 'express';
const router = Router();
@@ -6,13 +6,16 @@ router.get('/public', async (_req, reply) => {
return reply.json({ hello: 'world' });
});
-router.get('/home', function (_req, res) {
+const homeHandler: RequestHandler = function (_req, res) {
return res.render('home.ejs', {
publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
domain: process.env.CLERK_DOMAIN,
isSatellite: process.env.CLERK_IS_SATELLITE,
signInUrl: process.env.CLERK_SIGN_IN_URL,
});
-});
+};
+
+router.get('/home', homeHandler);
+router.get('/', homeHandler);
export const publicRoutes = router;
diff --git a/playground/express/src/server.ts b/playground/express/src/server.ts
index 2b3f27af550..c9efee78acc 100644
--- a/playground/express/src/server.ts
+++ b/playground/express/src/server.ts
@@ -1,6 +1,6 @@
-import type { StrictAuthProp } from '@clerk/clerk-sdk-node';
import type { Application, Request, Response, NextFunction } from 'express';
+import { clerkMiddleware } from '@clerk/express';
import * as express from 'express';
import { privateRoutes, publicRoutes } from './routes';
@@ -9,15 +9,11 @@ import './loadEnv';
const port = process.env.PORT || 3000;
const app: Application = express();
-declare global {
- namespace Express {
- interface Request extends StrictAuthProp {}
- }
-}
-
app.set('view engine', 'ejs');
app.set('views', 'src/views');
+app.use(clerkMiddleware());
+
app.use(publicRoutes);
app.use(privateRoutes);
diff --git a/scripts/canary.mjs b/scripts/canary.mjs
index 7335ead77ce..3823d94e666 100755
--- a/scripts/canary.mjs
+++ b/scripts/canary.mjs
@@ -20,6 +20,7 @@ const snapshot = `---
'@clerk/remix': patch
'@clerk/types': patch
'@clerk/clerk-expo': patch
+'@clerk/express': patch
---
Canary release
diff --git a/scripts/snapshot.mjs b/scripts/snapshot.mjs
index 9d5f6e85d3c..a2327b61eee 100755
--- a/scripts/snapshot.mjs
+++ b/scripts/snapshot.mjs
@@ -19,6 +19,7 @@ const snapshot = `---
'@clerk/remix': patch
'@clerk/types': patch
'@clerk/clerk-expo': patch
+'@clerk/express': patch
---
Snapshot release