Skip to content

Commit 55f58a0

Browse files
authored
fix: defer auth secret validation to runtime (#303)
1 parent 0943dad commit 55f58a0

6 files changed

Lines changed: 42 additions & 14 deletions

File tree

docs/content/3.guides/9.production-deployment.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Checklist and best practices for deploying Nuxt Better Auth in prod
55

66
## Environment Variables
77

8-
Production requires these environment variables:
8+
Production requires these environment variables at runtime:
99

1010
```ini [.env.production]
1111
# Required: 32+ character secret for session encryption
@@ -25,7 +25,7 @@ GOOGLE_CLIENT_SECRET="..."
2525
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
2626
```
2727

28-
The secret must be at least 32 characters. Shorter secrets will cause the module to throw an error.
28+
The secret must be at least 32 characters. Shorter secrets will cause auth initialization to throw an error at runtime.
2929

3030
## Security Checklist
3131

@@ -79,6 +79,10 @@ export default defineEventHandler((event) => {
7979

8080
For production, consider using Cloudflare rate limiting or a Redis-backed solution.
8181

82+
### Separate Build And Runtime Environments
83+
84+
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.
85+
8286
### Trusted Origins for Preview Environments
8387

8488
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({
117121

118122
### "NUXT_BETTER_AUTH_SECRET must be at least 32 characters"
119123

120-
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.
124+
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.
125+
126+
### "NUXT_BETTER_AUTH_SECRET is required in production"
127+
128+
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.
121129

122130
### "siteUrl required in production"
123131

src/module/runtime.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,6 @@ export function setupRuntimeConfig(input: SetupRuntimeConfigInput): { useHubKV:
6868
const currentSecret = nuxt.options.runtimeConfig.betterAuthSecret as string | undefined
6969
nuxt.options.runtimeConfig.betterAuthSecret = currentSecret || process.env.NUXT_BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET || ''
7070

71-
const betterAuthSecret = nuxt.options.runtimeConfig.betterAuthSecret as string
72-
if (!nuxt.options.dev && !nuxt.options._prepare && !betterAuthSecret) {
73-
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.')
74-
}
75-
if (betterAuthSecret && betterAuthSecret.length < 32) {
76-
throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET must be at least 32 characters for security')
77-
}
78-
7971
nuxt.options.runtimeConfig.auth = defu(nuxt.options.runtimeConfig.auth as Record<string, unknown>, {
8072
hubSecondaryStorage: options.hubSecondaryStorage ?? false,
8173
}) as AuthPrivateRuntimeConfig

src/runtime/server/utils/auth.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { getRequestHost, getRequestProtocol } from 'h3'
1010
import { useRuntimeConfig } from 'nitropack/runtime'
1111
import { withoutProtocol } from 'ufo'
1212
import { resolveCustomSecondaryStorageRequirement } from './custom-secondary-storage'
13+
import { validateAuthSecret } from './validate-secret'
1314

1415
type AuthOptions = ReturnType<typeof createServerAuth>
1516
type AuthInstance = ReturnType<typeof betterAuth<AuthOptions>>
@@ -256,6 +257,7 @@ export function serverAuth(event?: H3Event): AuthInstance {
256257
if (cached)
257258
return cached
258259

260+
const betterAuthSecret = validateAuthSecret(runtimeConfig.betterAuthSecret)
259261
const database = createDatabase()
260262
const userConfig = createServerAuth({ runtimeConfig, db }) as BetterAuthOptions & {
261263
secondaryStorage?: BetterAuthOptions['secondaryStorage']
@@ -275,7 +277,7 @@ export function serverAuth(event?: H3Event): AuthInstance {
275277
...userConfig,
276278
...(database && { database }),
277279
...(hubSecondaryStorage === true && { secondaryStorage: createSecondaryStorage() }),
278-
secret: runtimeConfig.betterAuthSecret,
280+
secret: betterAuthSecret,
279281
baseURL: siteUrl,
280282
trustedOrigins,
281283
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export function validateAuthSecret(secret: string | undefined): string {
2+
if (!import.meta.dev && !secret)
3+
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.')
4+
if (secret && secret.length < 32)
5+
throw new Error('[nuxt-better-auth] NUXT_BETTER_AUTH_SECRET must be at least 32 characters for security')
6+
7+
return secret || ''
8+
}

test/runtime-config.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe('setupRuntimeConfig secret resolution', () => {
135135
expect((nuxt.options as any).runtimeConfig.betterAuthSecret).toBe('fallback-secret-for-testing-only-32chars')
136136
})
137137

138-
it('throws in production when no secret is configured', () => {
138+
it('does not throw in production when no secret is configured', () => {
139139
const nuxt = createNuxtWithRuntimeConfig()
140140
nuxt.options.dev = false
141141
const consola = createConsolaMock()
@@ -147,7 +147,8 @@ describe('setupRuntimeConfig secret resolution', () => {
147147
databaseProvider: 'none',
148148
hasNuxtHub: false,
149149
consola,
150-
})).toThrow('NUXT_BETTER_AUTH_SECRET is required in production')
150+
})).not.toThrow()
151+
expect((nuxt.options as any).runtimeConfig.betterAuthSecret).toBe('')
151152
})
152153
})
153154

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { validateAuthSecret } from '../src/runtime/server/utils/validate-secret'
3+
4+
describe('validateAuthSecret', () => {
5+
it('throws when secret is missing', () => {
6+
expect(() => validateAuthSecret('')).toThrow('NUXT_BETTER_AUTH_SECRET is required in production')
7+
})
8+
9+
it('throws when secret is shorter than 32 characters', () => {
10+
expect(() => validateAuthSecret('too-short')).toThrow('NUXT_BETTER_AUTH_SECRET must be at least 32 characters')
11+
})
12+
13+
it('returns valid secret', () => {
14+
const secret = 'a'.repeat(32)
15+
expect(validateAuthSecret(secret)).toBe(secret)
16+
})
17+
})

0 commit comments

Comments
 (0)