UNPKG

@clerk/nextjs

Version:

Clerk SDK for NextJS

434 lines • 17.3 kB
import "../chunk-BUSYA2B4.js"; import { AuthStatus, constants, createBootstrapSignedOutState, createClerkRequest, createRedirect, getAuthObjectForAcceptedToken, isMachineTokenByPrefix, isTokenTypeAccepted, makeAuthObjectSerializable, TokenType } from "@clerk/backend/internal"; import { clerkFrontendApiProxy, DEFAULT_PROXY_PATH, matchProxyPath } from "@clerk/backend/proxy"; import { isProductionFromPublishableKey, parsePublishableKey } from "@clerk/shared/keys"; import { handleNetlifyCacheInDevInstance } from "@clerk/shared/netlifyCacheHandler"; import { isMalformedURLError } from "@clerk/shared/pathMatcher"; import { shouldAutoProxy } from "@clerk/shared/proxy"; import { notFound as nextjsNotFound } from "next/navigation"; import { NextResponse } from "next/server"; import { isRedirect, serverRedirectWithAuth, setHeader } from "../utils"; import { withLogger } from "../utils/debugLogger"; import { canUseKeyless } from "../utils/feature-flags"; import { clerkClient } from "./clerkClient"; import { DOMAIN, PROXY_URL, PUBLISHABLE_KEY, SECRET_KEY, SIGN_IN_URL, SIGN_UP_URL } from "./constants"; import { createContentSecurityPolicyHeaders } from "./content-security-policy"; import { errorThrower } from "./errorThrower"; import { getHeader } from "./headers-utils"; import { getKeylessCookieValue } from "./keyless"; import { clerkMiddlewareRequestDataStorage, clerkMiddlewareRequestDataStore } from "./middleware-storage"; import { isNextjsNotFoundError, isNextjsRedirectError, isNextjsUnauthorizedError, isRedirectToSignInError, isRedirectToSignUpError, nextjsRedirectError, redirectToSignInError, redirectToSignUpError, unauthorized } from "./nextErrors"; import { createProtect } from "./protect"; import { assertKey, decorateRequest, handleMultiDomainAndProxy, redirectAdapter, setRequestHeadersOnNextResponse } from "./utils"; const clerkMiddleware = ((...args) => { const [request, event] = parseRequestAndEvent(args); const [handler, params] = parseHandlerAndOptions(args); const middleware = clerkMiddlewareRequestDataStorage.run(clerkMiddlewareRequestDataStore, () => { const baseNextMiddleware = withLogger("clerkMiddleware", (logger) => async (request2, event2) => { const resolvedParams = typeof params === "function" ? await params(request2) : params; const keyless = await getKeylessCookieValue((name) => { var _a; return (_a = request2.cookies.get(name)) == null ? void 0 : _a.value; }); const publishableKey = assertKey( resolvedParams.publishableKey || PUBLISHABLE_KEY || (keyless == null ? void 0 : keyless.publishableKey), () => errorThrower.throwMissingPublishableKeyError() ); const secretKey = assertKey( resolvedParams.secretKey || SECRET_KEY || (keyless == null ? void 0 : keyless.secretKey), () => errorThrower.throwMissingSecretKeyError() ); const requestUrl = new URL(request2.nextUrl.href); let frontendApiProxyConfig = resolvedParams.frontendApiProxy; const hasExplicitProxyOrDomain = resolvedParams.proxyUrl || PROXY_URL || resolvedParams.domain || DOMAIN; if (!frontendApiProxyConfig && !hasExplicitProxyOrDomain && isProductionFromPublishableKey(publishableKey)) { if (shouldAutoProxy(requestUrl.hostname)) { frontendApiProxyConfig = { enabled: true }; } } if (frontendApiProxyConfig) { const { enabled, path: proxyPath = DEFAULT_PROXY_PATH } = frontendApiProxyConfig; const isEnabled = typeof enabled === "function" ? enabled(requestUrl) : enabled; if (isEnabled && matchProxyPath(request2, { proxyPath })) { return clerkFrontendApiProxy(request2, { proxyPath, publishableKey, secretKey }); } } const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL; const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL; const options = { publishableKey, secretKey, signInUrl, signUpUrl, ...resolvedParams }; clerkMiddlewareRequestDataStore.set("requestData", options); const resolvedClerkClient = await clerkClient(); if (options.debug) { logger.enable(); } const clerkRequest = createClerkRequest(request2); logger.debug("options", options); logger.debug("url", () => clerkRequest.toJSON()); const authHeader = request2.headers.get(constants.Headers.Authorization); if (authHeader && authHeader.startsWith("Basic ")) { logger.debug("Basic Auth detected"); } const cspHeader = request2.headers.get(constants.Headers.ContentSecurityPolicy); if (cspHeader) { logger.debug("Content-Security-Policy detected", () => ({ value: cspHeader })); } const requestState = await resolvedClerkClient.authenticateRequest( clerkRequest, createAuthenticateRequestOptions(clerkRequest, options) ); return runHandlerWithRequestState({ clerkRequest, request: request2, event: event2, requestState, handler, options, resolvedParams, keyless, logger }); }); const bootstrapNextMiddleware = withLogger("clerkMiddleware", (logger) => async (request2, event2) => { const resolvedParams = typeof params === "function" ? await params(request2) : params; const keyless = await getKeylessCookieValue((name) => { var _a; return (_a = request2.cookies.get(name)) == null ? void 0 : _a.value; }); const signInUrl = resolvedParams.signInUrl || SIGN_IN_URL || ""; const signUpUrl = resolvedParams.signUpUrl || SIGN_UP_URL || ""; const options = { publishableKey: "", secretKey: "", signInUrl, signUpUrl, ...resolvedParams }; clerkMiddlewareRequestDataStore.set("requestData", options); if (options.debug) { logger.enable(); } const clerkRequest = createClerkRequest(request2); logger.debug("keyless bootstrap (no publishable key)", () => ({ signInUrl, signUpUrl })); logger.debug("url", () => clerkRequest.toJSON()); const requestState = createBootstrapSignedOutState({ signInUrl, signUpUrl }); return runHandlerWithRequestState({ clerkRequest, request: request2, event: event2, requestState, handler, options, resolvedParams, keyless, logger }); }); const keylessMiddleware = async (request2, event2) => { var _a, _b; if (isKeylessSyncRequest(request2)) { return returnBackFromKeylessSync(request2); } const resolvedParams = typeof params === "function" ? await params(request2) : params; const keyless = await getKeylessCookieValue((name) => { var _a2; return (_a2 = request2.cookies.get(name)) == null ? void 0 : _a2.value; }); const isMissingPublishableKey = !(resolvedParams.publishableKey || PUBLISHABLE_KEY || (keyless == null ? void 0 : keyless.publishableKey)); const authHeader = (_b = (_a = getHeader(request2, constants.Headers.Authorization)) == null ? void 0 : _a.replace("Bearer ", "")) != null ? _b : ""; if (isMissingPublishableKey && !isMachineTokenByPrefix(authHeader)) { return bootstrapNextMiddleware(request2, event2); } return baseNextMiddleware(request2, event2); }; const nextMiddleware = async (request2, event2) => { if (canUseKeyless) { return keylessMiddleware(request2, event2); } return baseNextMiddleware(request2, event2); }; if (request && event) { return nextMiddleware(request, event); } return nextMiddleware; }); return middleware; }); const parseRequestAndEvent = (args) => { return [args[0] instanceof Request ? args[0] : void 0, args[0] instanceof Request ? args[1] : void 0]; }; const parseHandlerAndOptions = (args) => { return [ typeof args[0] === "function" ? args[0] : void 0, (args.length === 2 ? args[1] : typeof args[0] === "function" ? {} : args[0]) || {} ]; }; async function runHandlerWithRequestState({ clerkRequest, request, event, requestState, handler, options, resolvedParams, keyless, logger }) { var _a, _b; const { publishableKey, secretKey } = options; logger.debug("requestState", () => ({ status: requestState.status, headers: JSON.stringify(Object.fromEntries(requestState.headers)), reason: requestState.reason })); const locationHeader = requestState.headers.get(constants.Headers.Location); if (locationHeader) { handleNetlifyCacheInDevInstance({ locationHeader, requestStateHeaders: requestState.headers, publishableKey: requestState.publishableKey }); const res = NextResponse.redirect(requestState.headers.get(constants.Headers.Location) || locationHeader); requestState.headers.forEach((value, key) => { if (key === constants.Headers.Location) { return; } res.headers.append(key, value); }); return res; } else if (requestState.status === AuthStatus.Handshake) { throw new Error("Clerk: handshake status without redirect"); } const authObject = requestState.toAuth(); logger.debug("auth", () => ({ auth: authObject, debug: authObject.debug() })); const redirectToSignIn = createMiddlewareRedirectToSignIn(clerkRequest); const redirectToSignUp = createMiddlewareRedirectToSignUp(clerkRequest); const protect = await createMiddlewareProtect(clerkRequest, authObject, redirectToSignIn); const authHandler = createMiddlewareAuthHandler(requestState, redirectToSignIn, redirectToSignUp); authHandler.protect = protect; let handlerResult = NextResponse.next(); try { const userHandlerResult = await clerkMiddlewareRequestDataStorage.run( clerkMiddlewareRequestDataStore, async () => handler == null ? void 0 : handler(authHandler, request, event) ); handlerResult = userHandlerResult || handlerResult; } catch (e) { handlerResult = handleControlFlowErrors(e, clerkRequest, request, requestState); } if (options.contentSecurityPolicy) { const { headers } = createContentSecurityPolicyHeaders( ((_b = (_a = parsePublishableKey(publishableKey)) == null ? void 0 : _a.frontendApi) != null ? _b : "").replace("$", ""), options.contentSecurityPolicy ); const cspRequestHeaders = {}; headers.forEach(([key, value]) => { setHeader(handlerResult, key, value); cspRequestHeaders[key] = value; }); setRequestHeadersOnNextResponse(handlerResult, clerkRequest, cspRequestHeaders); logger.debug("Clerk generated CSP", () => ({ headers })); } if (requestState.headers) { requestState.headers.forEach((value, key) => { if (key === constants.Headers.ContentSecurityPolicy) { logger.debug("Content-Security-Policy detected", () => ({ value })); } handlerResult.headers.append(key, value); }); } if (isRedirect(handlerResult)) { logger.debug("handlerResult is redirect"); return serverRedirectWithAuth(clerkRequest, handlerResult, options); } if (options.debug) { setRequestHeadersOnNextResponse(handlerResult, clerkRequest, { [constants.Headers.EnableDebug]: "true" }); } const keylessKeysForRequestData = ( // Only pass keyless credentials when there are no explicit keys secretKey === (keyless == null ? void 0 : keyless.secretKey) ? { publishableKey: keyless == null ? void 0 : keyless.publishableKey, secretKey: keyless == null ? void 0 : keyless.secretKey } : {} ); decorateRequest( clerkRequest, handlerResult, requestState, resolvedParams, keylessKeysForRequestData, authObject.tokenType === "session_token" ? null : makeAuthObjectSerializable(authObject) ); return handlerResult; } const isKeylessSyncRequest = (request) => request.nextUrl.pathname === "/clerk-sync-keyless"; const returnBackFromKeylessSync = (request) => { const returnUrl = request.nextUrl.searchParams.get("returnUrl"); const url = new URL(request.url); url.pathname = ""; return NextResponse.redirect(returnUrl || url.toString()); }; const createAuthenticateRequestOptions = (clerkRequest, options) => { let resolvedOptions = options; if (options.frontendApiProxy && !options.proxyUrl) { const { enabled, path: proxyPath = DEFAULT_PROXY_PATH } = options.frontendApiProxy; const requestUrl = new URL(clerkRequest.url); const isEnabled = typeof enabled === "function" ? enabled(requestUrl) : enabled; if (isEnabled) { const derivedProxyUrl = `${requestUrl.origin}${proxyPath}`; resolvedOptions = { ...options, proxyUrl: derivedProxyUrl }; } } return { ...resolvedOptions, ...handleMultiDomainAndProxy(clerkRequest, resolvedOptions), // TODO: Leaving the acceptsToken as 'any' opens up the possibility of // an economic attack. We should revisit this and only verify a token // when auth() or auth.protect() is invoked. acceptsToken: "any" }; }; const createMiddlewareRedirectToSignIn = (clerkRequest) => { return (opts = {}) => { const url = clerkRequest.clerkUrl.toString(); redirectToSignInError(url, opts.returnBackUrl); }; }; const createMiddlewareRedirectToSignUp = (clerkRequest) => { return (opts = {}) => { const url = clerkRequest.clerkUrl.toString(); redirectToSignUpError(url, opts.returnBackUrl); }; }; const createMiddlewareProtect = (clerkRequest, rawAuthObject, redirectToSignIn) => { return (async (params, options) => { const notFound = () => nextjsNotFound(); const redirect = (url) => nextjsRedirectError(url, { redirectUrl: url }); const requestedToken = (params == null ? void 0 : params.token) || (options == null ? void 0 : options.token) || TokenType.SessionToken; const authObject = getAuthObjectForAcceptedToken({ authObject: rawAuthObject, acceptsToken: requestedToken }); return createProtect({ request: clerkRequest, redirect, notFound, unauthorized, authObject, redirectToSignIn })(params, options); }); }; const createMiddlewareAuthHandler = (requestState, redirectToSignIn, redirectToSignUp) => { const authHandler = async (options) => { var _a; const rawAuthObject = requestState.toAuth({ treatPendingAsSignedOut: options == null ? void 0 : options.treatPendingAsSignedOut }); const acceptsToken = (_a = options == null ? void 0 : options.acceptsToken) != null ? _a : TokenType.SessionToken; const authObject = getAuthObjectForAcceptedToken({ authObject: rawAuthObject, acceptsToken }); if (authObject.tokenType === TokenType.SessionToken && isTokenTypeAccepted(TokenType.SessionToken, acceptsToken)) { return Object.assign(authObject, { redirectToSignIn, redirectToSignUp }); } return authObject; }; return authHandler; }; const handleControlFlowErrors = (e, clerkRequest, nextRequest, requestState) => { var _a; if (isMalformedURLError(e)) { return new NextResponse(null, { status: 400, statusText: "Bad Request" }); } if (isNextjsUnauthorizedError(e)) { const response = new NextResponse(null, { status: 401 }); const authObject = requestState.toAuth(); if (authObject && authObject.tokenType === TokenType.OAuthToken) { const publishableKey = parsePublishableKey(requestState.publishableKey); return setHeader( response, "WWW-Authenticate", `Bearer resource_metadata="https://${publishableKey == null ? void 0 : publishableKey.frontendApi}/.well-known/oauth-protected-resource"` ); } return response; } if (isNextjsNotFoundError(e)) { return setHeader( // This is an internal rewrite purely to trigger a not found error. We do not want Next.js to think that the // destination URL is a valid page, so we use `nextRequest.url` as the base for the fake URL, which Next.js // understands is an internal URL and won't run middleware against the request. NextResponse.rewrite(new URL(`/clerk_${Date.now()}`, nextRequest.url)), constants.Headers.AuthReason, "protect-rewrite" ); } const isRedirectToSignIn = isRedirectToSignInError(e); const isRedirectToSignUp = isRedirectToSignUpError(e); if (isRedirectToSignIn || isRedirectToSignUp) { const redirect = createRedirect({ redirectAdapter, baseUrl: clerkRequest.clerkUrl, signInUrl: requestState.signInUrl, signUpUrl: requestState.signUpUrl, publishableKey: requestState.publishableKey, sessionStatus: (_a = requestState.toAuth()) == null ? void 0 : _a.sessionStatus, isSatellite: requestState.isSatellite }); const { returnBackUrl } = e; return redirect[isRedirectToSignIn ? "redirectToSignIn" : "redirectToSignUp"]({ returnBackUrl }); } if (isNextjsRedirectError(e)) { return redirectAdapter(e.redirectUrl); } throw e; }; export { clerkMiddleware, createAuthenticateRequestOptions }; //# sourceMappingURL=clerkMiddleware.js.map