nextauthjs / next-auth

Authentication for the Web.
https://authjs.dev
ISC License
24.65k stars 3.46k forks source link

AuthOptions ignores secret when initialized only during runtime #12053

Open BleedingDev opened 1 week ago

BleedingDev commented 1 week ago

Environment

System: OS: Windows 11 10.0.26120 CPU: (16) x64 AMD Ryzen 7 6800HS with Radeon Graphics Memory: 10.40 GB / 31.26 GB Binaries: Node: 20.17.0 - ~.proto\shims\node.EXE npm: 10.8.1 - ~.proto\shims\npm.EXE bun: 1.1.29 - ~.proto\shims\bun.EXE Browsers: Edge: Chromium (130.0.2849.13) Internet Explorer: 11.0.26100.1 npmPackages: @auth/prisma-adapter: ^2.6.0 => 2.7.0 next: 15.0.0-canary.170 => 15.0.0-canary.170 next-auth: ^4.24.8 => 4.24.8 react: 19.0.0-rc-1460d67c-20241003 => 19.0.0-rc-1460d67c-20241003

Reproduction URL

https://github.com/NaucMeIT/web/pull/465

Describe the issue

I decided to eliminate process.env from my codebase and have type-safe and secure secrets using Infisical + EffectTS, everything went smooth only except Vercel runtime check.

Here's my authOptions config:

import { PrismaAdapter } from '@auth/prisma-adapter'
import { prisma } from '@nmit-coursition/db'
import { secretsEnv } from '@nmit-coursition/env'
import bcrypt from 'bcryptjs'
import { Redacted } from 'effect'
import type { NextAuthOptions, User } from 'next-auth'
import type { Adapter } from 'next-auth/adapters'
import CredentialsProvider from 'next-auth/providers/credentials'
import GoogleProvider from 'next-auth/providers/google'

const handleAuthorize = async ({ email, password }: { email: string; password: string }) => {
  const user = await prisma.user.findFirst({ where: { email } })

  if (!user) return { error: 'user not found' }

  const isValidPassword = bcrypt.compareSync(password, user.password as string)

  if (!isValidPassword) return { error: 'invalid credentials' }

  return { id: user.id as string, email: user.email }
}

export const authOptions: NextAuthOptions = {
  adapter: PrismaAdapter(prisma) as Adapter,
  providers: [
    GoogleProvider({
      clientId: Redacted.value(secretsEnv.GOOGLE_ID),
      clientSecret: Redacted.value(secretsEnv.GOOGLE_SECRET),
    }),
    CredentialsProvider({
      credentials: {
        email: {},
        password: {},
      },
      async authorize(credentials, _) {
        const { email, password } = credentials as { email: string; password: string }

        return (await handleAuthorize({ email, password })) as User
      },
    }),
  ],
  pages: {
    signIn: '/sign-in',
    error: '/error',
  },
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    signIn: ({ user }) => {
      if ('error' in user) {
        return `/sign-in?error=${user.error}`
      }
      return true
    },
  },
  secret: Redacted.value(secretsEnv.NEXTAUTH_SECRET),
}

As you can see I am passing secret, but when NEXTAUTH_SECRET is not defined in secrets in Vercel's environment variables, it just doesn't work. I checked multiple times that during build-time the variable is defined and is correctly passed into the object.

But it still fails with this error:

[next-auth][error][NO_SECRET] 
https://next-auth.js.org/errors#no_secret Please define a `secret` in production. t [MissingSecretError]: Please define a `secret` in production.
    at t.assertConfig (/var/task/apps/coursition/.next/server/chunks/531.js:1:15201)
    at m (/var/task/apps/coursition/.next/server/chunks/531.js:1:8568)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async s (/var/task/apps/coursition/.next/server/chunks/531.js:25:20629)
    at async c (/var/task/apps/coursition/.next/server/chunks/128.js:6:7633) {
  code: 'NO_SECRET'
}

How to reproduce

  1. Use Vercel.
  2. Don't have NEXTAUTH_SECRET in Vercel
  3. Manually specify secret in authOptions.
  4. See it fail.

Demo of error: https://coursition-git-add-typesafe-config-naucmeits-projects.vercel.app/

Expected behavior

Normally working when manually passing secret in config.

balazsorban44 commented 1 week ago

Hi @BleedingDev!

This is kind of expected based on your code and not a NextAuth.js bug, just how JavaScript works, but I understand that it might be confusing at first sight, so let me try to explain.

Since NextAuth(authOptions) is outside the request handlers, our secret assertion logic inside NextAuth will run before the secret has been loaded into your runtime.

I actually don't think it's related to Vercel, try next build && next start locally, and you'll have the same issue.

You can solve this by using eg. advanced initialization: https://next-auth.js.org/configuration/initialization#advanced-initialization

I think this file needs to be changed like so:

import NextAuth from 'next-auth'
import { authOptions } from './auth-options'

- const handler = NextAuth(authOptions)

- export { handler as GET, handler as POST }

+ export const GET = (...args) => NextAuth(authOptions)(...args)
+ export const POST = (...args) => NextAuth(authOptions)(...args)

This will ensure that NextAuth is only invoked after your secrets have been initialized, passing the secret assertion logic!

Let me know if this resolves your issue!