diff --git a/docs/content/3.guides/9.production-deployment.md b/docs/content/3.guides/9.production-deployment.md index b57be28..b0605e6 100644 --- a/docs/content/3.guides/9.production-deployment.md +++ b/docs/content/3.guides/9.production-deployment.md @@ -5,7 +5,7 @@ description: Checklist and best practices for deploying Nuxt Better Auth in prod ## Environment Variables -Production requires these environment variables: +Production requires these environment variables at runtime: ```ini [.env.production] # Required: 32+ character secret for session encryption @@ -25,7 +25,7 @@ GOOGLE_CLIENT_SECRET="..." node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" ``` -The secret must be at least 32 characters. Shorter secrets will cause the module to throw an error. +The secret must be at least 32 characters. Shorter secrets will cause auth initialization to throw an error at runtime. ## Security Checklist @@ -79,6 +79,10 @@ export default defineEventHandler((event) => { For production, consider using Cloudflare rate limiting or a Redis-backed solution. +### Separate Build And Runtime Environments + +Some platforms, including Cloudflare deployments, expose build-time and runtime environment variables separately. `NUXT_BETTER_AUTH_SECRET` must be available to the deployed runtime, but it does not need to be present in the build container. + ### Trusted Origins for Preview Environments If your workflow uses preview URLs (for example, `*.workers.dev`), include those origins in your Better Auth server config. @@ -117,7 +121,11 @@ export default defineNuxtConfig({ ### "NUXT_BETTER_AUTH_SECRET must be at least 32 characters" -Your secret is too short. Generate a new one using the command above. `BETTER_AUTH_SECRET` is still accepted as a fallback, but `NUXT_BETTER_AUTH_SECRET` is the recommended variable. +Your secret is too short. Generate a new one using the command above. This error is raised when auth initializes at runtime. `BETTER_AUTH_SECRET` is still accepted as a fallback, but `NUXT_BETTER_AUTH_SECRET` is the recommended variable. + +### "NUXT_BETTER_AUTH_SECRET is required in production" + +The deployed server runtime could not resolve an auth secret. Set `NUXT_BETTER_AUTH_SECRET` or `BETTER_AUTH_SECRET` in the runtime environment for your app. ### "siteUrl required in production" diff --git a/src/module/runtime.ts b/src/module/runtime.ts index 3f682af..c19dafa 100644 --- a/src/module/runtime.ts +++ b/src/module/runtime.ts @@ -68,14 +68,6 @@ export function setupRuntimeConfig(input: SetupRuntimeConfigInput): { useHubKV: const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret as string | undefined nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.NUXT_BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET || '' - const betterAuthSecret = nuxt.options.runtimeConfig.betterAuthSecret as string - if (!nuxt.options.dev && !nuxt.options._prepare && !betterAuthSecret) { - throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET is required in production. Set NUXT_BETTER_AUTH_SECRET or BETTER_AUTH_SECRET environment variable.') - } - if (betterAuthSecret && betterAuthSecret.length < 32) { - throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET must be at least 32 characters for security') - } - nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth as Record, { hubSecondaryStorage: options.hubSecondaryStorage ?? false, }) as AuthPrivateRuntimeConfig diff --git a/src/runtime/server/utils/auth.ts b/src/runtime/server/utils/auth.ts index e98c29e..bea0c1f 100644 --- a/src/runtime/server/utils/auth.ts +++ b/src/runtime/server/utils/auth.ts @@ -10,6 +10,7 @@ import { getRequestHost, getRequestProtocol } from 'h3' import { useRuntimeConfig } from 'nitropack/runtime' import { withoutProtocol } from 'ufo' import { resolveCustomSecondaryStorageRequirement } from './custom-secondary-storage' +import { validateAuthSecret } from './validate-secret' type AuthOptions = ReturnType type AuthInstance = ReturnType> @@ -256,6 +257,7 @@ export function serverAuth(event?: H3Event): AuthInstance { if (cached) return cached + const betterAuthSecret = validateAuthSecret(runtimeConfig.betterAuthSecret) const database = createDatabase() const userConfig = createServerAuth({ runtimeConfig, db }) as BetterAuthOptions & { secondaryStorage?: BetterAuthOptions['secondaryStorage'] @@ -275,7 +277,7 @@ export function serverAuth(event?: H3Event): AuthInstance { ...userConfig, ...(database && { database }), ...(hubSecondaryStorage === true && { secondaryStorage: createSecondaryStorage() }), - secret: runtimeConfig.betterAuthSecret, + secret: betterAuthSecret, baseURL: siteUrl, trustedOrigins, }) diff --git a/src/runtime/server/utils/validate-secret.ts b/src/runtime/server/utils/validate-secret.ts new file mode 100644 index 0000000..0c3853b --- /dev/null +++ b/src/runtime/server/utils/validate-secret.ts @@ -0,0 +1,8 @@ +export function validateAuthSecret(secret: string | undefined): string { + if (!import.meta.dev && !secret) + throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET is required in production. Set NUXT_BETTER_AUTH_SECRET or BETTER_AUTH_SECRET environment variable.') + if (secret && secret.length < 32) + throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET must be at least 32 characters for security') + + return secret || '' +} diff --git a/test/runtime-config.test.ts b/test/runtime-config.test.ts index 7fd7dc2..4ec4843 100644 --- a/test/runtime-config.test.ts +++ b/test/runtime-config.test.ts @@ -135,7 +135,7 @@ describe('setupRuntimeConfig secret resolution', () => { expect((nuxt.options as any).runtimeConfig.betterAuthSecret).toBe('fallback-secret-for-testing-only-32chars') }) - it('throws in production when no secret is configured', () => { + it('does not throw in production when no secret is configured', () => { const nuxt = createNuxtWithRuntimeConfig() nuxt.options.dev = false const consola = createConsolaMock() @@ -147,7 +147,8 @@ describe('setupRuntimeConfig secret resolution', () => { databaseProvider: 'none', hasNuxtHub: false, consola, - })).toThrow('NUXT_BETTER_AUTH_SECRET is required in production') + })).not.toThrow() + expect((nuxt.options as any).runtimeConfig.betterAuthSecret).toBe('') }) }) diff --git a/test/server-auth-runtime-validation.test.ts b/test/server-auth-runtime-validation.test.ts new file mode 100644 index 0000000..8bb9b7d --- /dev/null +++ b/test/server-auth-runtime-validation.test.ts @@ -0,0 +1,17 @@ +import { describe, expect, it } from 'vitest' +import { validateAuthSecret } from '../src/runtime/server/utils/validate-secret' + +describe('validateAuthSecret', () => { + it('throws when secret is missing', () => { + expect(() => validateAuthSecret('')).toThrow('NUXT_BETTER_AUTH_SECRET is required in production') + }) + + it('throws when secret is shorter than 32 characters', () => { + expect(() => validateAuthSecret('too-short')).toThrow('NUXT_BETTER_AUTH_SECRET must be at least 32 characters') + }) + + it('returns valid secret', () => { + const secret = 'a'.repeat(32) + expect(validateAuthSecret(secret)).toBe(secret) + }) +})