nextauthjs / next-auth

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

getServerSession Is Always Null #6733

Closed Apestein closed 1 year ago

Apestein commented 1 year ago

Environment

System: OS: Windows 10 10.0.19044 CPU: (8) x64 AMD Ryzen 5 1400 Quad-Core Processor Memory: 12.59 GB / 15.90 GB Binaries: Node: 18.14.0 - C:\Program Files\nodejs\node.EXE npm: 9.2.0 - C:\Program Files\nodejs\npm.CMD Browsers: Edge: Spartan (44.19041.1266.0), Chromium (110.0.1587.46) Internet Explorer: 11.0.19041.1566

Reproduction URL

https://github.com/Apestein/dev-clubhouse/tree/bug-branch

Describe the issue

getServerSession always return null. Furthermore, it will logout if I refresh the page while logged in. And while getServerSession does not work, getSession does work as expect and session is returned. I'm not 100% sure this is a bug or if I'm missing something extremely obvious, but as I followed the docs I'm not sure what I did wrong if any.

import { NextApiRequest, NextApiResponse } from "next"
import dbConnect from "lib/dbConnect"
import Message from "@/models/Message"
import { authOptions } from "pages/api/auth/[...nextauth]"
import { getServerSession } from "next-auth/next"
import { getSession } from "next-auth/react"

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  await dbConnect()
  // const session = await getSession({ req })
  const session = await getServerSession(req, res, authOptions)
  console.log(session)
  const { method } = req

  switch (method) {
    case "GET":
      try {
        // const messages = session
        //   ? await Message.find({}, { __v: 0 })
        //   : await Message.find({}, { __v: 0, author: 0 })
        const messages = await Message.find({}, { __v: 0 })
        res.status(200).json(messages)
      } catch (error) {
        res.status(400).json({ success: false })
      }
      break
    case "POST":
      try {
        const message = await Message.create(req.body)
        res.status(201).json(message)
      } catch (error) {
        res.status(400).json({ success: false })
      }
      break
    case "PUT":
      try {
        const { _id, update } = req.body
        const message = await Message.updateOne(
          { _id: _id },
          { $set: { content: update } }
        )
        res.status(200).json({ success: message })
      } catch (error) {
        res.status(400).json({ success: false })
      }
      break
    case "DELETE":
      try {
        const id = req.body._id
        const message = await Message.deleteOne({ _id: id })
        res.status(200).json({ success: message })
      } catch (error) {
        res.status(400).json({ success: false })
      }
      break
    default:
      res.status(400).json({ success: false })
  }
}

How to reproduce

  1. clone repo
  2. npm install
  3. npm run dev
  4. login and check console

Update: I managed to fix it somehow but I don't know why. The bug is now in the bug-branch, and the main branch is now fixed and works.

Expected behavior

return the logged in user session

thearnica commented 1 year ago

Yes, I've run into the same problem today during migration to Next13. In Next13/app directory case it's a simple problem - one cannot use next-auth/react in ServerComponents, so only option is to use getServerSession from next-auth/next. But it does not work.

The problem is somewhere in JWT decode, it just fails, and I was not able to debug it as all my console.logs (🤷‍♀️) I've tried to use as breadcrumb were absent in the terminal. Probably something about experimental state of RSC, it still working a little strange.

Long story short - getSession and getServerSession works in a quite different ways, there is nothing similar between then, but more importantly the old getSession works, so I've merged the old code and the new to get working getServerSession 😎

The code is very bad as I had to copy-paste some internal variables, but it does work

import { fetchData } from "next-auth/client/_utils";
import { cookies, headers } from "next/headers";

// next-auth/utils is not listed in export, next will not let you import it
// duplicating
function parseUrl(url: string | undefined) {
  let _url2;

  const defaultUrl = new URL("http://localhost:3000/api/auth");

  if (url && !url.startsWith("http")) {
    url = `https://${url}`;
  }

  const _url = new URL(
    (_url2 = url) !== null && _url2 !== void 0 ? _url2 : defaultUrl,
  );

  const path = (
    _url.pathname === "/" ? defaultUrl.pathname : _url.pathname
  ).replace(/\/$/, "");
  const base = `${_url.origin}${path}`;

  return {
    origin: _url.origin,
    host: _url.host,
    path,
    base,
    toString: () => base,
  };
}

// local variable in `next-auth/react`
const __NEXTAUTH = {
  baseUrl: parseUrl(process.env.NEXTAUTH_URL ?? process.env.VERCEL_URL).origin,
  basePath: parseUrl(process.env.NEXTAUTH_URL).path,
  baseUrlServer: parseUrl(
    process.env.NEXTAUTH_URL_INTERNAL ??
      process.env.NEXTAUTH_URL ??
      process.env.VERCEL_URL,
  ).origin,
  basePathServer: parseUrl(
    process.env.NEXTAUTH_URL_INTERNAL ?? process.env.NEXTAUTH_URL,
  ).path,
  _lastSync: 0,
  _session: undefined,
  _getSession: () => {
    // nope
  },
};

const logger = {
  error: console.error,
  warn: console.warn,
  debug: console.log,
};

export const getServerSession = async () => {
// code from `next-auth/next` for RSC
  const req: any = {
    headers: Object.fromEntries(headers()),
    cookies: Object.fromEntries(
      cookies()
        .getAll()
        .map((c) => [c.name, c.value]),
    ),
  };

// the old `next-auth/react` getSession
  const session = await fetchData("session", __NEXTAUTH, logger, { req });

  return session;
};
Apestein commented 1 year ago

@thearnica Yes, you are right. It was some error about JWT failing to decode. However the weird thing is since I'm actively working on this repo, I manage to accidently fix it somehow but I don't know what I did.

Georgi-Stavrev commented 1 year ago

@thearnica Yes, you are right. It was some error about JWT failing to decode. However the weird thing is since I'm actively working on this repo, I manage to accidently fix it somehow but I don't know what I did.

Please tell me you can track it down, It's not behaving as expected on Next 13, despite implementing it per the documentation and various guides.

thearnica commented 1 year ago

So the stack trace:

  stack: 'JWEDecryptionFailed: decryption operation failed\n' +
    '    at gcmDecrypt (webpack-internal:///(sc_server)/../../../node_modules/jose/dist/node/esm/runtime/decrypt.js:81:15)\n' +
    '    at decrypt (webpack-internal:///(sc_server)/../../../node_modules/jose/dist/node/esm/runtime/decrypt.js:104:20)\n' +
    '    at flattenedDecrypt (webpack-internal:///(sc_server)/../../../node_modules/jose/dist/node/esm/jwe/flattened/decrypt.js:157:90)\n' +
    '    at async compactDecrypt (webpack-internal:///(sc_server)/../../../node_modules/jose/dist/node/esm/jwe/compact/decrypt.js:56:23)\n' +
    '    at async jwtDecrypt (webpack-internal:///(sc_server)/../../../node_modules/jose/dist/node/esm/jwt/decrypt.js:46:23)\n' +
    '    at async Object.decode (webpack-internal:///(sc_server)/../../../node_modules/next-auth/jwt/index.js:44:26)\n' +
    '    at async Object.session (webpack-internal:///(sc_server)/../../../node_modules/next-auth/core/routes/session.js:59:34)\n' +
    '    at async AuthHandler (webpack-internal:///(sc_server)/../../../node_modules/next-auth/core/index.js:221:37)\n' +
    '    at async getServerSession (webpack-internal:///(sc_server)/../../../node_modules/next-auth/next/index.js:123:21)\n' +
    '    at async SessionProvider (webpack-internal:///(sc_server)/./app/layout.tsx:22:21)',
  name: 'JWEDecryptionFailed'

next-auth uses Jose v 4.11.0, downgrading locally does not help

Later I've put my token into token.dev and it was also not able to parse it, the error is "alg A256GCM is not supported". All other online JWT decodes I was able to find also failed me. https://dinochiesa.github.io/jwt/ worked the best, but still was not able to help.

Something is not right there

Apestein commented 1 year ago

I think I figured out the problem. Trying adding the NEXTAUTH_SECRET environment variable. https://next-auth.js.org/deployment let me know if it works

ethanmick commented 1 year ago

Yes, setting NEXTAUTH_SECRET locally (and in production) should resolve this issue.

Apestein commented 1 year ago

Yes, setting NEXTAUTH_SECRET locally (and in production) should resolve this issue.

Ok, glad it worked. But that was not mentioned anywhere in the docs. It should be mentioned in getServerSession ideally. And the stack trace errors didn't help at all.

namanUIUC commented 1 year ago

I'm new to NextJS and Next-Auth. I'm trying to write a secure api route that is only available if a user is logged in. I successfully accessing the session on the client side using useSession() but when I try to implement the logic in an api route the session always returns null. I have tried to copy the simplest example from the docs. Am I missing something?

Here is my route in src/pages/api/test.ts:

import { getServerSession } from 'next-auth/next'
import { authOptions } from './auth/[...nextauth]'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  const session = await getServerSession(req, res, authOptions)
  console.log('session', session)

  if (session) {
    res.send({ content: 'SUCCESS' })
  } else {
    res.send({ error: 'ERROR' })
  }
}

Here is my authOptions in src/pages/api/auth/[...nextauth].ts:

import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";

export const authOptions = {
  // Configure one or more authentication providers
  providers: [
    GoogleProvider({
      clientId: process.env.AUTH_GOOGLE_ID,
      clientSecret: process.env.AUTH_GOOGLE_SECRET,
    }),
    // ...add more providers here
  ],
};

export default NextAuth(authOptions);

Also, I have NEXTAUTH_URL and NEXTAUTH_SECRET env values set locally.

thearnica commented 1 year ago

NEXTAUTH_SECRET resolves the issue. The only moment not to forget - wire NextAuthOptions to it. Wondering if one would make first argument non optional to enforce the "match" between loging-in experience and getSession

burnedikt commented 1 year ago

If I'm not mistaken, this issue also affects the public example of next-auth / auth.js at https://next-auth-example.vercel.app/server. At least I don't see my session's details ever on the server-rendered example meaning it can only be null. Works fine on the client-rendered or api-based examples. In my case, also setting NEXTAUTH_SECRET did not solve the issue (next 13.2.4, next-auth 4.20.1).

formidabilus commented 1 year ago

If I'm not mistaken, this issue also affects the public example of next-auth / auth.js at https://next-auth-example.vercel.app/server. At least I don't see my session's details ever on the server-rendered example meaning it can only be null. Works fine on the client-rendered or api-based examples. In my case, also setting NEXTAUTH_SECRET did not solve the issue (next 13.2.4, next-auth 4.20.1).

Same here, I still get null session with getServerSession(authOptions) even with NEXTAUTH_SECRET set in .env.

Using: "next": "13.2.3", "next-auth": "^4.20.1"

burnedikt commented 1 year ago

If I'm not mistaken, this issue also affects the public example of next-auth / auth.js at https://next-auth-example.vercel.app/server. At least I don't see my session's details ever on the server-rendered example meaning it can only be null. Works fine on the client-rendered or api-based examples. In my case, also setting NEXTAUTH_SECRET did not solve the issue (next 13.2.4, next-auth 4.20.1).

I did some more digging and seems like this issue is already known over at the example app: https://github.com/nextauthjs/next-auth-example/pull/81. It's not directly a next-auth issue but just confusing / accidental forwarding of the session prop. Basically, the (server-side) session set as a prop by getServerSideProps is never forwarded to the ServerSidePage component as _app.tsx "strips" the session prop through destructuring.

Therefore, the SessionProvider receives the server-side session as intended but the ServerSidePage never gets it.

It's also worth noting this is already addressed in the examples project for nextjs within the main next-auth repo but for some reason not yet mirrored to the dedicated example repository.

danieltorres1109 commented 1 year ago

This Work for me const session: any = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });