UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

1 lines 10.9 kB
{"version":3,"file":"origin-check.mjs","names":["logBackwardCompatWarning","trustedOrigins: string[]"],"sources":["../../../src/api/middlewares/origin-check.ts"],"sourcesContent":["import type { GenericEndpointContext } from \"@better-auth/core\";\nimport { createAuthMiddleware } from \"@better-auth/core/api\";\nimport { BASE_ERROR_CODES } from \"@better-auth/core/error\";\nimport { deprecate } from \"@better-auth/core/utils\";\nimport { APIError } from \"better-call\";\nimport { matchesOriginPattern } from \"../../auth/trusted-origins\";\n\n/**\n * Checks if CSRF should be skipped for backward compatibility.\n * Previously, disableOriginCheck also disabled CSRF checks.\n * This maintains that behavior when disableCSRFCheck isn't explicitly set.\n */\nfunction shouldSkipCSRFForBackwardCompat(ctx: GenericEndpointContext): boolean {\n\treturn (\n\t\tctx.context.skipOriginCheck &&\n\t\tctx.context.options.advanced?.disableCSRFCheck === undefined\n\t);\n}\n\n/**\n * Logs deprecation warning for users relying on coupled behavior.\n * Only logs if user explicitly set disableOriginCheck (not test environment default).\n */\nconst logBackwardCompatWarning = deprecate(\n\tfunction logBackwardCompatWarning() {},\n\t\"disableOriginCheck: true currently also disables CSRF checks. \" +\n\t\t\"In a future version, disableOriginCheck will ONLY disable URL validation. \" +\n\t\t\"To keep CSRF disabled, add disableCSRFCheck: true to your config.\",\n);\n\n/**\n * A middleware to validate callbackURL and origin against trustedOrigins.\n * Also handles CSRF protection using Fetch Metadata for first-login scenarios.\n */\nexport const originCheckMiddleware = createAuthMiddleware(async (ctx) => {\n\t// Skip origin check for GET, OPTIONS, HEAD requests - we don't mutate state here.\n\tif (\n\t\tctx.request?.method === \"GET\" ||\n\t\tctx.request?.method === \"OPTIONS\" ||\n\t\tctx.request?.method === \"HEAD\" ||\n\t\t!ctx.request\n\t) {\n\t\treturn;\n\t}\n\tawait validateOrigin(ctx);\n\n\tif (ctx.context.skipOriginCheck) {\n\t\treturn;\n\t}\n\n\tconst { body, query } = ctx;\n\tconst callbackURL = body?.callbackURL || query?.callbackURL;\n\tconst redirectURL = body?.redirectTo;\n\tconst errorCallbackURL = body?.errorCallbackURL;\n\tconst newUserCallbackURL = body?.newUserCallbackURL;\n\n\tconst validateURL = (url: string | undefined, label: string) => {\n\t\tif (!url) {\n\t\t\treturn;\n\t\t}\n\t\tconst isTrustedOrigin = ctx.context.isTrustedOrigin(url, {\n\t\t\tallowRelativePaths: label !== \"origin\",\n\t\t});\n\n\t\tif (!isTrustedOrigin) {\n\t\t\tctx.context.logger.error(`Invalid ${label}: ${url}`);\n\t\t\tctx.context.logger.info(\n\t\t\t\t`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\\n`,\n\t\t\t\t`Current list of trustedOrigins: ${ctx.context.trustedOrigins}`,\n\t\t\t);\n\t\t\tthrow new APIError(\"FORBIDDEN\", { message: `Invalid ${label}` });\n\t\t}\n\t};\n\n\tcallbackURL && validateURL(callbackURL, \"callbackURL\");\n\tredirectURL && validateURL(redirectURL, \"redirectURL\");\n\terrorCallbackURL && validateURL(errorCallbackURL, \"errorCallbackURL\");\n\tnewUserCallbackURL && validateURL(newUserCallbackURL, \"newUserCallbackURL\");\n});\n\nexport const originCheck = (\n\tgetValue: (ctx: GenericEndpointContext) => string | string[],\n) =>\n\tcreateAuthMiddleware(async (ctx) => {\n\t\tif (!ctx.request) {\n\t\t\treturn;\n\t\t}\n\t\tif (ctx.context.skipOriginCheck) {\n\t\t\treturn;\n\t\t}\n\t\tconst callbackURL = getValue(ctx);\n\t\tconst validateURL = (url: string | undefined, label: string) => {\n\t\t\tif (!url) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst isTrustedOrigin = ctx.context.isTrustedOrigin(url, {\n\t\t\t\tallowRelativePaths: label !== \"origin\",\n\t\t\t});\n\n\t\t\tif (!isTrustedOrigin) {\n\t\t\t\tctx.context.logger.error(`Invalid ${label}: ${url}`);\n\t\t\t\tctx.context.logger.info(\n\t\t\t\t\t`If it's a valid URL, please add ${url} to trustedOrigins in your auth config\\n`,\n\t\t\t\t\t`Current list of trustedOrigins: ${ctx.context.trustedOrigins}`,\n\t\t\t\t);\n\t\t\t\tthrow new APIError(\"FORBIDDEN\", { message: `Invalid ${label}` });\n\t\t\t}\n\t\t};\n\t\tconst callbacks = Array.isArray(callbackURL) ? callbackURL : [callbackURL];\n\t\tfor (const url of callbacks) {\n\t\t\tvalidateURL(url, \"callbackURL\");\n\t\t}\n\t});\n\n/**\n * Validates origin header against trusted origins.\n * @param ctx - The endpoint context\n * @param forceValidate - If true, always validate origin regardless of cookies/skip flags\n */\nasync function validateOrigin(\n\tctx: GenericEndpointContext,\n\tforceValidate = false,\n): Promise<void> {\n\tconst headers = ctx.request?.headers;\n\tif (!headers || !ctx.request) {\n\t\treturn;\n\t}\n\tconst originHeader = headers.get(\"origin\") || headers.get(\"referer\") || \"\";\n\tconst useCookies = headers.has(\"cookie\");\n\n\tif (ctx.context.skipCSRFCheck) {\n\t\treturn;\n\t}\n\n\tif (shouldSkipCSRFForBackwardCompat(ctx)) {\n\t\tctx.context.options.advanced?.disableOriginCheck === true &&\n\t\t\tlogBackwardCompatWarning();\n\t\treturn;\n\t}\n\n\tconst shouldValidate = forceValidate || useCookies;\n\n\tif (!shouldValidate) {\n\t\treturn;\n\t}\n\n\tif (!originHeader || originHeader === \"null\") {\n\t\tthrow new APIError(\"FORBIDDEN\", {\n\t\t\tmessage: BASE_ERROR_CODES.MISSING_OR_NULL_ORIGIN,\n\t\t});\n\t}\n\n\tconst trustedOrigins: string[] = Array.isArray(\n\t\tctx.context.options.trustedOrigins,\n\t)\n\t\t? ctx.context.trustedOrigins\n\t\t: [\n\t\t\t\t...ctx.context.trustedOrigins,\n\t\t\t\t...((await ctx.context.options.trustedOrigins?.(ctx.request))?.filter(\n\t\t\t\t\t(v): v is string => Boolean(v),\n\t\t\t\t) || []),\n\t\t\t];\n\n\tconst isTrustedOrigin = trustedOrigins.some((origin) =>\n\t\tmatchesOriginPattern(originHeader, origin),\n\t);\n\tif (!isTrustedOrigin) {\n\t\tctx.context.logger.error(`Invalid origin: ${originHeader}`);\n\t\tctx.context.logger.info(\n\t\t\t`If it's a valid URL, please add ${originHeader} to trustedOrigins in your auth config\\n`,\n\t\t\t`Current list of trustedOrigins: ${trustedOrigins}`,\n\t\t);\n\t\tthrow new APIError(\"FORBIDDEN\", { message: \"Invalid origin\" });\n\t}\n}\n\n/**\n * Middleware for CSRF protection using Fetch Metadata headers.\n * This prevents cross-site navigation login attacks while supporting progressive enhancement.\n */\nexport const formCsrfMiddleware = createAuthMiddleware(async (ctx) => {\n\tconst request = ctx.request;\n\tif (!request) {\n\t\treturn;\n\t}\n\n\tawait validateFormCsrf(ctx);\n});\n\n/**\n * Validates CSRF protection for first-login scenarios using Fetch Metadata headers.\n * This prevents cross-site form submission attacks while supporting progressive enhancement.\n */\nasync function validateFormCsrf(ctx: GenericEndpointContext): Promise<void> {\n\tconst req = ctx.request;\n\tif (!req) {\n\t\treturn;\n\t}\n\n\tif (ctx.context.skipCSRFCheck) {\n\t\treturn;\n\t}\n\n\tif (shouldSkipCSRFForBackwardCompat(ctx)) {\n\t\treturn;\n\t}\n\n\tconst headers = req.headers;\n\tconst hasAnyCookies = headers.has(\"cookie\");\n\n\tif (hasAnyCookies) {\n\t\treturn await validateOrigin(ctx);\n\t}\n\n\tconst site = headers.get(\"Sec-Fetch-Site\");\n\tconst mode = headers.get(\"Sec-Fetch-Mode\");\n\tconst dest = headers.get(\"Sec-Fetch-Dest\");\n\n\tconst hasMetadata = Boolean(\n\t\t(site && site.trim()) || (mode && mode.trim()) || (dest && dest.trim()),\n\t);\n\n\tif (hasMetadata) {\n\t\t// Block cross-site navigation requests (classic CSRF attack pattern)\n\t\tif (site === \"cross-site\" && mode === \"navigate\") {\n\t\t\tctx.context.logger.error(\n\t\t\t\t\"Blocked cross-site navigation login attempt (CSRF protection)\",\n\t\t\t\t{\n\t\t\t\t\tsecFetchSite: site,\n\t\t\t\t\tsecFetchMode: mode,\n\t\t\t\t\tsecFetchDest: dest,\n\t\t\t\t},\n\t\t\t);\n\t\t\tthrow new APIError(\"FORBIDDEN\", {\n\t\t\t\tmessage: BASE_ERROR_CODES.CROSS_SITE_NAVIGATION_LOGIN_BLOCKED,\n\t\t\t});\n\t\t}\n\n\t\treturn await validateOrigin(ctx, true);\n\t}\n\n\t// No cookies, no Fetch Metadata → fallback to old behavior (no validation)\n\treturn;\n}\n"],"mappings":"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"}