UNPKG

@clerk/express

Version:

Clerk server SDK for usage with Express

307 lines (296 loc) • 11.9 kB
import { a as requestHasAuthObject, i as loadClientEnv, n as incomingMessageToRequest, o as requestToProxyRequest, r as loadApiEnv, t as brandRequestAuth } from "./utils-DtX1zZA7.mjs"; import { createClerkClient } from "@clerk/backend"; import { Readable } from "stream"; import { AuthStatus, createClerkRequest, getAuthObjectForAcceptedToken } from "@clerk/backend/internal"; import { DEFAULT_PROXY_PATH, clerkFrontendApiProxy, stripTrailingSlashes } from "@clerk/backend/proxy"; import { isDevelopmentFromSecretKey } from "@clerk/shared/keys"; import { logger } from "@clerk/shared/logger"; import { isHttpOrHttps, isProxyUrlRelative, isValidProxyUrl } from "@clerk/shared/proxy"; import { handleValueOrFn } from "@clerk/shared/utils"; import { deprecated } from "@clerk/shared/deprecated"; export * from "@clerk/backend" //#region src/clerkClient.ts let clerkClientSingleton = {}; const clerkClient = new Proxy(clerkClientSingleton, { get(_target, property) { if (property in clerkClientSingleton) return clerkClientSingleton[property]; const env = { ...loadApiEnv(), ...loadClientEnv() }; const client = createClerkClient({ ...env, userAgent: `@clerk/express@2.1.40` }); if (env.secretKey) clerkClientSingleton = client; return client[property]; }, set() { return false; } }); //#endregion //#region src/errors.ts const createErrorMessage = (msg) => { 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 `; }; const middlewareRequired = (fnName) => 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()); `); const satelliteAndMissingProxyUrlAndDomain = "Missing domain and proxyUrl. A satellite application needs to specify a domain or a proxyUrl"; 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.`; //#endregion //#region src/authenticateRequest.ts /** * @internal * Authenticates an Express request by wrapping clerkClient.authenticateRequest and * converts the express request object into a standard web request object * * @param opts - Configuration options for request authentication * @param opts.clerkClient - The Clerk client instance to use for authentication * @param opts.request - The Express request object to authenticate * @param opts.options - Optional middleware configuration options */ const authenticateRequest = (opts) => { const { clerkClient, request, options } = opts; const { clerkClient: _clerkClient, debug: _debug, frontendApiProxy: _frontendApiProxy, isSatellite: isSatelliteInput, domain: domainInput, signInUrl: signInUrlInput, proxyUrl: proxyUrlInput, secretKey: secretKeyInput, machineSecretKey: machineSecretKeyInput, publishableKey: publishableKeyInput, ...restOptions } = options || {}; const clerkRequest = createClerkRequest(incomingMessageToRequest(request)); const env = { ...loadApiEnv(), ...loadClientEnv() }; const secretKey = secretKeyInput || env.secretKey; const machineSecretKey = machineSecretKeyInput || env.machineSecretKey; const publishableKey = publishableKeyInput || env.publishableKey; const isSatellite = handleValueOrFn(isSatelliteInput, clerkRequest.clerkUrl, env.isSatellite); const domain = handleValueOrFn(domainInput, clerkRequest.clerkUrl) || env.domain; const signInUrl = signInUrlInput || env.signInUrl; const proxyUrl = absoluteProxyUrl(handleValueOrFn(proxyUrlInput, 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, { ...restOptions, secretKey, machineSecretKey, publishableKey, proxyUrl, isSatellite, domain, signInUrl }); }; const setResponseHeaders = (requestState, res) => { 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, res) => { if (requestState.headers.get("location")) { res.status(307).end(); return; } if (requestState.status === AuthStatus.Handshake) return /* @__PURE__ */ new Error("Clerk: unexpected handshake without redirect"); }; const absoluteProxyUrl = (relativeOrAbsoluteUrl, baseUrl) => { if (!relativeOrAbsoluteUrl || !isValidProxyUrl(relativeOrAbsoluteUrl) || !isProxyUrlRelative(relativeOrAbsoluteUrl)) return relativeOrAbsoluteUrl; return new URL(relativeOrAbsoluteUrl, baseUrl).toString(); }; const resolveDefaultClerkClient = (options) => { if (!options.apiUrl && !options.apiVersion) return clerkClient; return createClerkClient({ ...loadApiEnv(), ...loadClientEnv(), ...options.apiUrl ? { apiUrl: options.apiUrl } : {}, ...options.apiVersion ? { apiVersion: options.apiVersion } : {}, userAgent: `@clerk/express@2.1.40` }); }; const authenticateAndDecorateRequest = (options = {}) => { const clerkClient = options.clerkClient || resolveDefaultClerkClient(options); const frontendApiProxy = options.frontendApiProxy; const proxyPath = stripTrailingSlashes(frontendApiProxy?.path ?? DEFAULT_PROXY_PATH) || DEFAULT_PROXY_PATH; const middleware = async (request, response, next) => { if (requestHasAuthObject(request)) return next(); if ("auth" in request) logger.warnOnce("Clerk: another middleware has already set `req.auth` on this request. Clerk authentication will run anyway and overwrite it. To use another auth library alongside Clerk, configure it to store its state on a different request property."); const env = { ...loadApiEnv(), ...loadClientEnv() }; const publishableKey = options.publishableKey || env.publishableKey; const secretKey = options.secretKey || env.secretKey; if (frontendApiProxy) { const requestUrl = new URL(request.originalUrl || request.url, `http://${request.headers.host}`); if ((typeof frontendApiProxy.enabled === "function" ? frontendApiProxy.enabled(requestUrl) : frontendApiProxy.enabled) && (requestUrl.pathname === proxyPath || requestUrl.pathname.startsWith(proxyPath + "/"))) { const proxyResponse = await clerkFrontendApiProxy(requestToProxyRequest(request), { proxyPath, publishableKey, secretKey }); response.status(proxyResponse.status); proxyResponse.headers.forEach((value, key) => { response.setHeader(key, value); }); if (proxyResponse.body) { const reader = proxyResponse.body.getReader(); new Readable({ async read() { try { const { done, value } = await reader.read(); if (done) this.push(null); else this.push(Buffer.from(value)); } catch (error) { this.destroy(error instanceof Error ? error : new Error(String(error))); } } }).pipe(response); } else response.end(); return; } } let resolvedOptions = options; if (frontendApiProxy && !options.proxyUrl) { const requestUrl = new URL(request.originalUrl || request.url, `http://${request.headers.host}`); if (typeof frontendApiProxy.enabled === "function" ? frontendApiProxy.enabled(requestUrl) : frontendApiProxy.enabled) resolvedOptions = { ...options, proxyUrl: proxyPath }; } try { const requestState = await authenticateRequest({ clerkClient, request, options: resolvedOptions }); const err = setResponseHeaders(requestState, response); if (err) return next(err); if (response.writableEnded) return; const auth = brandRequestAuth((opts) => requestState.toAuth(opts)); Object.assign(request, { auth }); next(); } catch (err) { next(err); } }; return middleware; }; //#endregion //#region src/clerkMiddleware.ts /** * Middleware that integrates Clerk authentication into your Express application. * It checks the request's cookies and headers for a session JWT and, if found, * attaches the Auth object to the request object under the `auth` key. * * Accepts either a static options object or a callback that receives the request * and returns options. The callback form is useful for multi-domain setups where * the publishable key differs per domain. * * @example * app.use(clerkMiddleware(options)); * * @example * const clerkClient = createClerkClient({ ... }); * app.use(clerkMiddleware({ clerkClient })); * * @example * app.use(clerkMiddleware()); * * @example * // Dynamic keys per domain * app.use(clerkMiddleware((req) => ({ * publishableKey: req.hostname === 'example.com' ? PK_A : PK_B, * }))); */ const clerkMiddleware = (options = {}) => { if (typeof options !== "function") { const authMiddleware = authenticateAndDecorateRequest({ ...options, acceptsToken: "any" }); return (request, response, next) => { authMiddleware(request, response, next); }; } return async (request, response, next) => { try { authenticateAndDecorateRequest({ ...await options(request), acceptsToken: "any" })(request, response, next); } catch (err) { next(err); } }; }; //#endregion //#region src/getAuth.ts /** * Retrieves the Clerk AuthObject using the current request object. * * @param {GetAuthOptions} options - Optional configuration for retrieving auth object. * @returns {AuthObject} Object with information about the request state and claims. * @throws {Error} `clerkMiddleware` or `requireAuth` is required to be set in the middleware chain before this util is used. */ const getAuth = ((req, options) => { if (!requestHasAuthObject(req)) throw new Error(middlewareRequired("getAuth")); return getAuthObjectForAcceptedToken({ authObject: req.auth(options), acceptsToken: options?.acceptsToken }); }); //#endregion //#region src/requireAuth.ts /** * Middleware to require authentication for user requests. * Redirects unauthenticated requests to the sign-in url. * * @deprecated Use `clerkMiddleware()` with `getAuth()` instead. * `requireAuth` will be removed in the next major version. * * @example * // Before (deprecated) * import { requireAuth } from '@clerk/express' * router.get('/path', requireAuth(), getHandler) * * @example * // After (recommended) * import { clerkMiddleware, getAuth } from '@clerk/express' * * app.use(clerkMiddleware()) * * app.get('/api/protected', (req, res) => { * const { userId } = getAuth(req); * if (!userId) { * return res.status(401).json({ error: 'Unauthorized' }); * } * // handle authenticated request * }) */ const requireAuth = (options = {}) => { const authMiddleware = authenticateAndDecorateRequest({ ...options, acceptsToken: "any" }); return (request, response, next) => { deprecated("requireAuth", "Use `clerkMiddleware()` with `getAuth()` instead. `requireAuth` will be removed in the next major version."); authMiddleware(request, response, (err) => { if (err) return next(err); const signInUrl = options.signInUrl || process.env.CLERK_SIGN_IN_URL || "/"; if (!request.auth()?.userId) return response.redirect(signInUrl); next(); }); }; }; //#endregion export { authenticateRequest, clerkClient, clerkMiddleware, getAuth, requireAuth }; //# sourceMappingURL=index.mjs.map