Skip to content

Navigation Menu

Sign in
Appearance settings
Sign up
Appearance settings
Discussion options

I am in need of help when it comes to logging in with credentails.

i am trying to implement next-auth in my project and managed to make 3rd party auth (Google, Github) work.
I implemented the CredentialsProvider, everything went fine with JWT sessions but i cant seem to get the session when i'm using a database strategy.

here is my nextAuth:

export default NextAuth({
  providers: [
    CredentialsProvider({
      name: 'NSID',
      credentials: {
        username: { label: 'Username', type: 'text', placeholder: 'NSID' },
        password: {
          label: 'Password',
          type: 'password',
          placeholder: 'Password',
        },
      },
      async authorize(credentials, req) {
        const user = await prisma.user.findUnique({
          where: { nsid: credentials?.username },
        })

        if (user && validatePassword(user, credentials!.password)) {
          return {
            id: user.id,
            name: user.name,
            email: user.email,
            nsid: user.nsid,
            image: user.image,
            location: user.location,
          }
        }
        // Return null if user data could not be retrieved
        return null
      },
    }),
    Google({
      clientId: process.env.GOOGLE_ID,
      clientSecret: process.env.GOOGLE_SECRET,
    }),
    Github({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
  adapter: PrismaAdapter(prisma),
  secret: process.env.NEXT_AUTH_SECRET,
  callbacks: {
    async signIn({ user, account, profile, email, credentials }) {
      if (user) {
        return true
      }
      return false
    },
    async redirect({ url, baseUrl }) {
      if (url.startsWith(baseUrl)) return url
      else if (url.startsWith('/')) return new URL(url, baseUrl).toString()
      return baseUrl
    },
    async session({ session, token, user }) {
      if (token) {
        session.id = token.id
      }
      return session
    },
    async jwt({ token, user, account, profile, isNewUser }) {
      if (user) {
        token.id = user.id
      }

      return token
    },
  },
  pages: {
    error: '/',
  },
  debug: true,

  session: {
    strategy: 'database',
    maxAge: 30 * 24 * 60 * 60, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
  },
})

Can anyone give me some advice on how to proceed forward please?

You must be logged in to vote

I was able to use database strategy with credentials provider and everything else works perfectly.
Thanks to @nneko

here is the comment he provided:
#4394 (reply in thread)

and his blogpost about the issue:
https://nneko.branche.online/next-auth-credentials-provider-with-the-database-session-strategy/

Replies: 36 comments · 106 replies

Comment options

according to their documentation you can't do this you have to use JWT as the session strategy which then means you can't use any other Provider

You must be logged in to vote
0 replies
Comment options

Link to the docs: https://next-auth.js.org/providers/credentials
So the answer is to ditch the credentials login in order to have the session persisted?
I've managed to make credentials work, and now I want to implement some sort of online user search bar

You must be logged in to vote
12 replies
@AaronMBMorse
Comment options

I am getting this error. Any ideas?

Not sure how to get around that. Maybe there's a way to disable that error?

[next-auth][error][CALLBACK_CREDENTIALS_JWT_ERROR]
https://next-auth.js.org/errors#callback_credentials_jwt_error Signin in with credentials only supported if JWT strategy is enabled UnsupportedStrategy [UnsupportedStrategyError]: Signin in with credentials only supported if JWT strategy is enabled

@Ahmadh26
Comment options

@AaronMBMorse

That is because you're not using JWT strategy in nextAuth options
Check this out. You have to use strategy: JWT rather than database so that you can use credentials login

@AaronMBMorse
Comment options

Thank you @Ahmadh26. I saw that when I was looking through the docs.

Isn't the objective of what @nneko wrote to have credentials with the database strategy?

If you set the strategy to jwt and have a adapter does it store the session data in the database? Assuming it does, can you sign someone out before the jwt token expires?

The project requires the ability to end sessions on the server so we can't use normal jwt. There needs to be something stored server side that gets check to make sure some permissions haven't changed.

@nneko
Comment options

@AaronMBMorse yes the patch above works and is intended for you to use with a "database" strategy. However, you haven't shared enough details for how you have nextauth setup for me to provide any help. The **session** property in the nextauth options should look something like the below:

session: {
    strategy: "database", // Store sessions in the database and store a sessionToken in the cookie for lookups
    jwt: false,
    maxAge: 30 * 24 * 60 * 60, // 30 days to session expiry
    updateAge: 24 * 60 * 60, // 24 hours to update session data into database
}
@AaronMBMorse
Comment options

@nneko here is my next auth file with the additions you recommended.

Right now when I send a post request to /api/auth/signin I am getting the error that says I have to use jwt for credentials.

[next-auth][error][CALLBACK_CREDENTIALS_JWT_ERROR]

`
import NextAuth, { NextAuthOptions } from 'next-auth'
import type { NextApiRequest, NextApiResponse } from 'next'

// Config
import { DATABASE_URL, SESSION_SECRET } from '@config'

// Utils
import { SnakeNamingStrategy } from 'typeorm-naming-strategies'

// Adapter
import { TypeORMLegacyAdapter } from '@next-auth/typeorm-legacy-adapter'

// Providers
import CredentialsProvider from 'next-auth/providers/credentials'

// Entities
import { AppDataSource } from '@data-source'
import { User } from '@Entities'

import { randomUUID } from 'crypto'
import Cookies from 'cookies'
import { encode, decode } from 'next-auth/jwt'
import { Provider } from 'next-auth/providers'

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const generateSessionToken = () => {
return randomUUID()
}

const fromDate = (time: number, date = Date.now()) => {
return new Date(date + time * 1000)
}

const adapter = TypeORMLegacyAdapter({
type: 'postgres',
url: DATABASE_URL,
synchronize: false, // DO NOT CHANGE ME (Can Delete Entire DB)
namingStrategy: new SnakeNamingStrategy()
})

const providers: Provider[] = [
CredentialsProvider({
name: 'credentials',
credentials: {
email: {
label: 'Email',
type: 'email',
placeholder: 'testing@gmail.com'
},
password: {
label: 'Password',
type: 'password'
}
},
authorize: async credentials => {
try {
const { email, password } = credentials as { email: string; password: string }

      // Fail if there is missing credentials
      if (!email || !password) {
        return null
      }

      // Initialize data source
      if (!AppDataSource.isInitialized) {
        await AppDataSource.initialize()
      }
      const userRepo = AppDataSource.getRepository(User)

      // Try to find a user
      const user = await userRepo.findOne({ where: { email } })

      if (user) {
        console.log('Authorization success')

        return {
          id: user.id,
          name: 'first last'
        }
      }

      // Failed to find user
      return null
    } catch (e) {
      console.log('Authorization error: ', e)
      return null
    }
  }
})

]

const authOptions: NextAuthOptions = {
adapter,
providers,
secret: SESSION_SECRET,
session: {
strategy: 'database',
maxAge: 30 * 24 * 60 * 60, // 30 Days
updateAge: 24 * 60 * 60 // 1 Day
},
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.id = user.id
}
return token
},
session: async ({ session, token }) => {
if (token) {
session.id = token.id
}
return session
},
signIn: async ({ user, account, profile, email, credentials }) => {
if (req.query.nextauth!.includes('callback') && req.query.nextauth!.includes('credentials') && req.method === 'POST') {
if (user) {
const sessionToken = generateSessionToken()
const sessionMaxAge = 60 * 60 * 24 * 30 // 30 Days
const sessionExpiry = fromDate(sessionMaxAge)

        await adapter.createSession({
          sessionToken: sessionToken,
          userId: user.id,
          expires: sessionExpiry
        })

        const cookies = new Cookies(req, res)

        cookies.set('next-auth.session-token', sessionToken, {
          expires: sessionExpiry
        })
      }
    }
    return true
  }
},
jwt: {
  encode: async ({ token, secret, maxAge }) => {
    if (req.query.nextauth!.includes('callback') && req.query.nextauth!.includes('credentials') && req.method === 'POST') {
      const cookies = new Cookies(req, res)
      const cookie = cookies.get('next-auth.session-token')

      if (cookie) return cookie
      else return ''
    }
    // Revert to default behaviour when not in the credentials provider callback flow
    return encode({ token, secret, maxAge })
  },
  decode: async ({ token, secret }) => {
    if (req.query.nextauth!.includes('callback') && req.query.nextauth!.includes('credentials') && req.method === 'POST') {
      return null
    }

    // Revert to default behaviour when not in the credentials provider callback flow
    return decode({ token, secret })
  }
},
pages: {
  signIn: '/signin'
},
debug: true

}

return await NextAuth(req, res, authOptions)
}
`

Comment options

Hi @nneko,

first of all, I would like to say "thank you" for your blog post "https://branche.online/next-auth-credentials-provider-with-the-database-session-strategy/" and your comments/posts on this discussion. I followed all steps, but I am running into the same issue as mentioned by @AaronMBMorse ([next-auth][error][CALLBACK_CREDENTIALS_JWT_ERROR]). Attached are all the involved files and I would really appreciate your help to fix this last piece. I'm using next-auth 4.10.3 and node 16.16.0

api/auth/[...nextauth].js

import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import GithubProvider from "next-auth/providers/github";
import GoogleProvider from "next-auth/providers/google";
import prisma from "../../../prisma/prisma";
// Modules needed to support key generation, token encryption, and HTTP cookie manipulation
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { randomUUID } from "crypto";
import Cookies from "cookies";
import { encode, decode } from "next-auth/jwt";

export default async function handler(req, res) {
  let userAccount = null;

  const bcrypt = require("bcrypt");

  const confirmPasswordHash = (plainPassword, hashedPassword) => {
    return new Promise((resolve) => {
      bcrypt.compare(plainPassword, hashedPassword, function (err, res) {
        resolve(res);
      });
    });
  };

  const generateSessionToken = () => {
    return randomUUID?.() ?? generate.uuid();
  };

  const adapter = PrismaAdapter(prisma);

  const fromDate = (time, date = Date.now()) => {
    return new Date(date + time * 1000);
  };

  const generate = {};
  generate.uuid = function () {
    return uuidv4();
  };

  generate.uuidv4 = function () {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) =>
      (
        c ^
        (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))
      ).toString(16)
    );
  };

  const callbacks = {
    async signIn({ user, account, profile, email, credentials }) {
      console.log("User Signin Start: ", user);
      // Check if this sign in callback is being called in the credentials authentication flow. If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
      if (
        req.query.nextauth.includes("callback") &&
        req.query.nextauth.includes("credentials") &&
        req.method === "POST"
      ) {
        if (user) {
          const sessionToken = generateSessionToken(); // Implement a function to generate the session token (you can use randomUUID as an example)
          // const sessionExpiry = fromDate(session.maxAge); // Implement a function to calculate the session cookie expiry date
          const sessionMaxAge = 60 * 60 * 24 * 30; //30Days
          const sessionExpiry = fromDate(sessionMaxAge); // Implement a function to calculate the session cookie expiry date
          // console.log(sessionExpiry);

          await adapter.createSession({
            sessionToken: sessionToken,
            userId: user.id,
            expires: sessionExpiry,
          });

          const cookies = new Cookies(req, res);

          cookies.set("next-auth.session-token", sessionToken, {
            expires: sessionExpiry,
          });

          console.log("user Session: ", user);
        }
      }

      return true;
    },
    async register(firstName, lastName, email, password) {
      try {
        await prisma.User.create({
          data: {
            firstName: firstName,
            lastName: lastName,
            username: email,
            email: email,
            password: password,
          },
        });
        return true;
      } catch (err) {
        console.error("Failed to register user. Error", err);
        return false;
      }
    },
    async jwt(token, user, account, profile, isNewUser) {
      // console.log("JWT callback. Got User: ", user);
      // if (typeof user !== typeof undefined) {
      //   token.user = user;
      // }
      // return Promise.resolve(token);
      if (user) {
        token.id = user.id;
      }

      return token;
    },
    async session(session, token) {
      if (token) {
        session.id = token.id;
      }
      // // console.log("Session. Got User: ", session, token);
      // if (userAccount !== null) {
      //   //session.user = userAccount;
      //   // console.log("UserAccount Session Generation: ", userAccount);
      //   session.user = {
      //     id: userAccount.id,
      //     name: `${userAccount.firstName} ${userAccount.lastName}`,
      //     email: userAccount.email,
      //   };
      //   // console.log("Session.user: ", session.user);
      // } else if (
      //   typeof token.user !== typeof undefined &&
      //   (typeof session.user === typeof undefined ||
      //     (typeof session.user !== typeof undefined &&
      //       typeof session.user.id === typeof undefined))
      // ) {
      //   session.user = token.user;
      // } else if (typeof token !== typeof undefined) {
      //   session.token = token;
      // }
      // console.log("Session: ", session);
      // return Promise.resolve(session);
      return session;
    },
  };

  const options = {
    session: {
      // strategy: "jwt",
      strategy: "database", // Store sessions in the database and store a sessionToken in the cookie for lookups
      jwt: false,

      // Seconds - How long until an idle session expires and is no longer valid.
      maxAge: 30 * 24 * 60 * 60, // 30 days

      // Seconds - Throttle how frequently to write to database to extend a session.
      // Use it to limit write operations. Set to 0 to always update the database.
      // Note: This option is ignored if using JSON Web Tokens
      updateAge: 24 * 60 * 60, // 24 hours
    },
    jwt: {
      // Customize the JWT encode and decode functions to overwrite the default behaviour of storing the JWT token in the session  cookie when using credentials providers. Instead we will store the session token reference to the session in the database.
      encode: async ({ token, secret, maxAge }) => {
        if (
          req.query.nextauth.includes("callback") &&
          req.query.nextauth.includes("credentials") &&
          req.method === "POST"
        ) {
          const cookies = new Cookies(req, res);

          // console.log("Cookies: ", cookies);

          const cookie = cookies.get("next-auth.session-token");

          console.log("pure Cookie: ", cookie);

          if (cookie) return cookie;
          else return "";
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return encode(token, secret, maxAge);
      },
      decode: async ({ token, secret }) => {
        if (
          req.query.nextauth.includes("callback") &&
          req.query.nextauth.includes("credentials") &&
          req.method === "POST"
        ) {
          return null;
        }

        // Revert to default behaviour when not in the credentials provider callback flow
        return decode(token, secret);
      },
    },
    debug: process.env.NODE_ENV === "development",
    adapter,
    secret: process.env.NEXTAUTH_SECRET,
    logger: {
      error(code, metadata) {
        console.log({ type: "inside error logger", code, metadata });
      },
      warn(code) {
        console.log({ type: "inside warn logger", code });
      },
      debug(code, metadata) {
        console.log({ type: "inside debug logger", code, metadata });
      },
    },
    providers: [
      CredentialsProvider({
        name: "credentials",
        credentials: {},
        async authorize(credentials) {
          try {
            const user = await prisma.User.findUnique({
              where: {
                email: credentials.email,
              },
            });

            console.log("Authorize User Credentials: ", user);

            if (user !== null) {
              //Compare the hash
              const res = await confirmPasswordHash(
                credentials.password,
                user.password
              );
              if (res === true) {
                userAccount = {
                  id: user.id,
                  firstName: user.firstName,
                  lastName: user.lastName,
                  email: user.email,
                  isActive: user.isActive,
                };
                // console.log("UserAccount created: ", userAccount);
                return userAccount;
              } else {
                console.log("Hash not matched logging in");
                return null;
              }
            } else {
              return null;
            }
          } catch (err) {
            console.log("Authorize error:", err);
          }
        },
      }),
      // GithubProvider({
      //   clientId: process.env.GITHUB_ID,
      //   clientSecret: process.env.GITHUB_SECRET,
      // }),
      // GoogleProvider({
      //   clientId: process.env.GOOGLE_CLIENT_ID,
      //   clientSecret: process.env.GOOGLE_CLIENT_SECRET,
      // }),
    ],
    callbacks: callbacks,
  };

  return await NextAuth(req, res, options);
}

api/auth/signup.js

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const bcrypt = require("bcrypt");

export default async function handler(req, res) {
  try {
    switch (req.method) {
      case "POST":
        // console.log(req.body);
        const {
          firstName,
          lastName,
          email,
          password,
          confirm,
          username,
          csrfToken,
        } = req.body;

        if (!(email && password && confirm && password.length >= 1)) {
          console.log("Missing Parameter");
          res.status(400).json({
            statusText: "Invalid user parameters",
          });
          break;
        }

        if (password != confirm) {
          console.log("Confirm Error");
          return res.status(400).json({
            statusText: "Password mismatch",
          });

          break;
        }

        console.log(email);

        const profileExists = await prisma.User.findUnique({
          where: {
            email: email,
          },
        });       

        console.log(profileExists);

        if ( 
          profileExists
        ) {
          console.log("Profile already exists");
          res.status(403).json({
            statusText: "User already exists",
          });
          break;
        }

        const hash = await bcrypt.hash(password, 0);
        console.log("Username: ", hash, firstName, lastName, email, hash);
        const user = await prisma.User.create({
          data: {
            firstName: firstName,
            lastName: lastName,
            name: lastName + " " + firstName,
            email: email,
            username: email,
            password: hash,
            isActive: "1",
          },
        });

        if (!user) {
          res.status(500).json({
            statusText: "Unable to create user account",
          });
        }

        const account = await prisma.Account.create({
          data: {
            userId: user.id,
            type: "credentials",
            provider: "credentials",
            providerAccountId: user.id,
          },
        });

        if (user && account) {
          res.status(200).json({
            id: user.id,
            name: user.name,
            email: user.email,
          });
        } else {
          res.status(500).json({
            statusText: "Unable to link account to created user profile",
          });
        }

        // console.log("RES: ", res);

        return res;

      // break;

      default:
        res.setHeader("Allow", ["POST"]);
        res
          .status(405)
          .json({ statusText: `Method ${req.method} Not Allowed` });
    }
  } catch (err) {
    return res.status(503).json({ err: err.toString() });
  }
}

pages/register.js

import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
const bcrypt = require("bcrypt");

export default async (req, res) => {
  if (req.method === "POST") {
    const { firstName, lastName, email, password } = req.body;

    try {
      const hash = await bcrypt.hash(password, 0);
      await prisma.User.create({
        data: {
          firstName: firstName,
          lastName: lastName,
          name: lastName + " " + firstName,
          email: email,
          password: hash,
        },
      });

      return res.status(200).end();
    } catch (err) {
      return res.status(503).json({ err: err.toString() });
    }
  } else {
    return res
      .status(405)
      .json({ error: "This request only supports POST requests" });
  }
};

pages/login.js

import Container from "../components/container";
import Layout from "../components/layout";
import { useState } from "react";
import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import Head from "next/head";
import { getAllNavitemsForHome, getAllFooter } from "../lib/api";
import { COMPANY, PRECOMPANY } from "../lib/constants";

export default function Login({ preview, menuItems, footerItems }) {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loginError, setLoginError] = useState("");

  const router = useRouter();

  const handleLogin = (event) => {
    event.preventDefault();
    event.stopPropagation();

    signIn("credentials", {
      email,
      password,
      callbackUrl: `${window.location.origin}/`,
      redirect: false,
    }).then(function (result) {
      if (result.error !== null) {
        if (result.status === 401) {
          setLoginError(
            "Your username/password combination was incorrect. Please try again"
          );
        } else {
          setLoginError(result.error);
        }
      } else {
        router.push(result.url);
      }
    });
  };

  return (
    <>
      <Layout preview={preview} menuItems={menuItems} footerItems={footerItems}>
        <Head>
          <title>
            {PRECOMPANY} {COMPANY}
          </title>
        </Head>
        <Container>
          <h3 className="text-sm font-semibold tracking-wider text-gray-400 uppercase">
            Melde dich an
          </h3>
          <div className="mt-4 sm:flex sm:max-w-md">
            <form onSubmit={handleLogin}>
              {loginError}
              <label>
                Email Adresse
                <input
                  type="email"
                  value={email}
                  onChange={(e) => setEmail(e.target.value)}
                  placeholder="Enter your email"
                  autoComplete="email"
                  required
                  className="w-full min-w-0 px-4 py-2 text-base text-gray-900 placeholder-gray-500 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:placeholder-gray-400"
                />
              </label>
              <label>
                Password
                <input
                  type="password"
                  value={password}
                  onChange={(e) => setPassword(e.target.value)}
                  placeholder="Enter your password"
                  required
                  className="w-full min-w-0 px-4 py-2 text-base text-gray-900 placeholder-gray-500 border border-gray-300 rounded-md shadow-sm appearance-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:placeholder-gray-400"
                />
              </label>
              <div className="mt-3 rounded-md sm:mt-0 sm:flex-shrink-0">
                <button
                  type="submit"
                  className="w-full min-w-0 px-4 py-2 mt-4 bg-indigo-600 btn btn-primary"
                >
                  Submit login
                </button>
              </div>
            </form>
          </div>
        </Container>
      </Layout>
    </>
  );
}

export async function getStaticProps({ preview = false }) {
  const data = await getAllNavitemsForHome(preview);
  const footeritems = await getAllFooter(preview);

  // console.log(data?.navItems)

  return {
    props: {
      preview,
      menuItems: data?.navItems ?? null,
      footerItems: footeritems ?? null,
    },
  };
}
You must be logged in to vote
14 replies
@shtse8
Comment options

@AaronMBMorse Hey! Really appreciate you diving into why server-side is the go-to for hashing passwords. It’s super insightful and definitely highlights some key points about security and keeping things smooth for users. 🛡️

But, you know, I've been turning a thought around in my head. While I totally get where you're coming from, there's this one thing that keeps nagging me. When we only hash passwords on the server-side, there's this sneaky risk of plaintext passwords getting a bit too cozy with the server environment. Imagine, just for a sec, that they end up in logs, memory dumps, or—knock on wood—the server gets compromised right before the hashing magic happens. It's kinda spooky thinking that even the most well-meaning server admins might accidentally get a peek at those passwords. And let's not even start on the external breach boogeyman. 😱

So, here's a thought: What if we mix it up and do a bit of both? Like a security layer cake!

First Layer - Client-Side Hash: We start with hashing the password on the user's side. It's like putting a disguise on the password before it even leaves the house. That way, the real password never has to take that risky trip over the internet in its birthday suit.

Second Layer - Server-Side Magic: Once our disguised password arrives at the server, we go for round two of hashing (and don’t forget the salt!). It's like adding another layer of disguise, so even if someone gets their hands on it, they’re miles away from the real deal.

I’m thinking this could be our secret sauce to keep things extra secure without making it a hassle. It's like having both a belt and suspenders—why not, right?

Keen to hear what you think! Could this be our middle ground, or is there a plot twist I’m missing?

@seh-GAH-toh
Comment options

@shtse8 In this scenario, you have two options:

  • In my approach, I use the full-stack framework Nuxt 3. This allows me to manage data transactions internally within the framework, avoiding external network requests. I can securely handle credentials, validate, salt, hash, and store data in the database without it ever leaving the confines of the full-stack application.
  • Alternatively, you can utilize tools such as Uncrypto, which offers a unified, platform-agnostic cryptographic interface. With this option, you can encrypt user data on the client-side before sending it to the backend. Upon receipt, the data can be decrypted, validated, and processed accordingly. It's essential to employ additional tools like Infiscal to manage sensitive information such as cryptographic keys securely.

Regarding the concern about data ending up in logs, memory dumps, or other unintended locations, I lack significant experience in those specific scenarios to offer the best approach for handling them effectively.

Furthermore, when considering hashing passwords on the client and then again on the server, it's crucial to note that certain hashing algorithms may not handle nested hashing efficiently. There have been issues reported with some algorithms (I don't recall whether it was bcrypt, scrypt, or argon2), encountering problems with nested hashing, potentially leading to password integrity issues.

However, as the saying goes, "the strength of a chain is limited to that of the weakest link." This underscores the importance of not only employing robust cryptographic measures but also ensuring the security of the overall environment. Factors such as poorly handled keys or the presence of malicious actors within your development team can compromise the effectiveness of even the most stringent cryptographic practices.

In essence, it ultimately aligns with the guidance provided in the auth.js documentation, which intentionally restricts the functionality for credentials-based authentication. This limitation serves to discourage reliance on passwords due to their inherent security risks and the added complexity associated with supporting usernames and passwords.

@shtse8
Comment options

Thank you for sharing your insights into working with security using Nuxt 3 and other tools like Uncrypto and Infiscal. It's clear that you have a solid understanding of the complexities of data security and encryption, which is really reassuring.

I share your sentiments about using Nuxt 3 and Auth to create a seamless development experience while maintaining strong security. That’s what we’re all striving for, right? The perfect balance between development efficiency and robust security measures.

What you mentioned about the potential pitfalls of relying solely on password credentials resonates very much with me. This is a complex issue and one that really should be moving away from traditional password-based authentication. The way you manage data transactions within the framework to minimize external vulnerabilities is very clever.

I've been thinking a lot about my past practices, specifically hashing passwords in Vue component scripts before sending them to API endpoints. My original intention for doing this was to add an extra layer of security from the user side. However, your insights have made me rethink and evaluate the effectiveness and potential risks of this approach.

Thinking about sending passwords, potentially reused in critical services like banking or mail, directly to the server does raise significant trust issues. This is a real problem faced by many users, who knowingly or unknowingly risk their security due to password reuse. This is why I prefer a client-side hashing approach - at least ensuring that the original password is not exposed directly during transmission or server processing.

What you mention about the challenges of handling nested hashes, and the specific algorithms that may not be well suited to handling it, are particularly illuminating. This is a reminder of the delicate balance we need to maintain when applying security measures to ensure they don’t inadvertently weaken the system.

Adopting a Zero Trust philosophy seems more relevant than ever, not only in the way we design systems, but also in how we handle the trust dynamics between service providers and users. Your discussion definitely inspired a lot of thinking on my part about how to better balance these considerations in my work.

Thanks so much for this exchange, @ArthurSegato . This was really informative and thought provoking. Look forward to more discussions like this and exploring how we can continue to keep user trust and security top of mind while protecting our apps.

It was really a pleasure to communicate!

@OllyMan21
Comment options

Hi @shtse8, I know this is an old topic but it might be useful for other readers. The purpose of hashing the password server-side is to prevent the instance in which the database itself gets compromised one cannot then use the retrieved hashed password to login to someone's account. As you can imagine, if one were to retrieve the hashed password from the database and then try and login to someone's account, they would send the hashed password to the server which would then get hashed again and wouldn't evaluate to the same value stored in the database. If the password is hashed client side then the server would be storing, from its perspective, a plain text password. If a malicious user were to then acquire that password from the database they could simply bypass the hashing function clientside and send the hashed password directly and get acces to the account. You already mentioned that we could hash the password on both sides which is a solution but in my opinion it is unnecessary security. I understand your concern would be that the password could get logged but that is something that should never happen and if it did then the company would be liable.

Hopefully that explains the purpose of hashing server-side. Hashing clientside defeats the purpose, hashing serverside is the intended purpose, hashing both sides is feasible but imo unnecessary.

@shtse8
Comment options

Hi @OllyMan21 ,

Thank you for your detailed explanation. I understand your perspective on hashing passwords server-side. The reasoning you provided makes sense, particularly in the context of preventing attackers from using stolen hashed passwords to access accounts. Hashing client-side indeed wouldn't offer the same level of protection since the hashed password could be used directly if intercepted.

However, there is a different cultural perspective to consider, particularly in regions where there's a significant mistrust of servers and companies handling sensitive information. This is especially true in Chinese culture, where people are more cautious about potential data breaches and the misuse of their passwords.

Many users tend to reuse passwords across multiple sites and services. If their passwords are sent to the server without any encryption, there's a fear that the company could potentially misuse these passwords to access other accounts, such as personal email accounts. This concern leads to a lack of trust in websites that do not encrypt passwords before sending them to the server, with such sites often being seen as fraudulent or phishing attempts.

Given these concerns, it’s common for people here to prefer client-side hashing before sending passwords to the server, even if it might be perceived as unnecessary security from a technical standpoint.

I hope this provides some insight into the cultural differences and the reasons behind the preference for client-side hashing in some regions.

Best regards,
Kyle

Comment options

hi @nneko could you please re-post your reply so i can mark it as an answer? with a link to your blog post? :) not sure how to mark a reply as an answer lol.

You must be logged in to vote
0 replies
Comment options

I just ended a proof of concept in which Database Session (Prisma Adapter) is used with Credential Provider and Github Provider from NextAuth without the usage of JWTs, just Session tokens and it now works well, most of the code is from @nneko blog post, however I didn't used pwd encryption and stored rawly in the db, just for the sake of being simple

Also, instead of using normal API routes, I ended up using tRPC from the create-t3-app scaffold, however there isn't too much difference tbh

I also deployed to vercel, I needed to use PostgreSQL because SQLite doesnt work well with Vercel, however on the localhost SQLite works properly.

you can see all the code on this repo I made

again, huge thanks to everyone on this thread

You must be logged in to vote
0 replies
Comment options

Has anyone been able to deploy this on Vercel, or any other provider?
because in my local environment it works fine, but in production the session is always an empty object.

You must be logged in to vote
6 replies
@mattiaz9
Comment options

thank you @abehidek.
Yours seems very similar to mine:

import type { NextApiRequest, NextApiResponse } from "next"
import NextAuth from "next-auth"
import type { NextAuthOptions, SessionOptions } from "next-auth"
import type { Adapter } from "next-auth/adapters"
import { encode, decode } from "next-auth/jwt"
import CredentialsProvider from "next-auth/providers/credentials"
import GoogleProvider from "next-auth/providers/google"
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import type { User } from "@prisma/client"
import bcrypt from "bcryptjs"
import Cookies from "cookies"
import { randomUUID } from "crypto"

import { env } from "@/env/server"
import { prisma } from "@/server/prisma"
import { parseTenantDomain } from "@/utils/routes"

const getAdapter = (req: NextApiRequest, res: NextApiResponse): Adapter => ({
  ...PrismaAdapter(prisma),
  async getSessionAndUser(sessionToken) {
    console.log("PRE SESSION", sessionToken)
    const tenant =
      (req.query.tenant as string) ?? parseTenantDomain(req.headers.referer ?? req.url!)
    const userAndSession = await prisma.session.findUnique({
      where: { sessionToken },
      include: {
        user: {
          select: {
            id: true,
            email: true,
            name: true,
            image: true,
            agencies: {
              where: {
                agency: {
                  domain: tenant ?? "",
                },
              },
              select: {
                agency: {
                  select: {
                    domain: true,
                  },
                },
                role: true,
              },
            },
          },
        },
      },
    })
    console.log("SESSION USER", sessionToken, userAndSession)
    if (!userAndSession) return null

    const role = userAndSession.user.agencies.find(a => a.agency.domain === tenant)?.role

    const { user, ...session } = userAndSession
    const userWithRole: any = { ...user, role }
    return { user: userWithRole, session }
  },
})

const session: Partial<SessionOptions> = {
  strategy: "database",
  maxAge: 30 * 24 * 60 * 60, // 30 days
  updateAge: 24 * 60 * 60, // 24 hours
}

export const authOptions = (req: NextApiRequest, res: NextApiResponse): NextAuthOptions => {
  const adapter = getAdapter(req, res)
  return {
    callbacks: {
      session({ session, user }) {
        console.log("SESSION", session, user)
        if (session.user) {
          session.user.id = user.id
          session.user.role = user.role as string
        }
        return session
      },
      async signIn({ user }) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          if (user && "id" in user) {
            const sessionToken = randomUUID()
            const sessionExpiry = new Date(Date.now() + session.maxAge! * 1000)
            const forwarded = req.headers["x-forwarded-for"] as string
            const ip = forwarded ? forwarded.split(/, /)[0] : req.connection.remoteAddress

            await adapter.createSession({
              sessionToken,
              userId: user.id,
              expires: sessionExpiry,
              userAgent: req.headers["user-agent"] ?? null,
              ip,
            } as any)

            const cookies = new Cookies(req, res)
            cookies.set("next-auth.session-token", sessionToken, {
              expires: sessionExpiry,
            })
          }
        }

        return true
      },
    },
    jwt: {
      encode(params) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          const cookies = new Cookies(req, res)
          const cookie = cookies.get("next-auth.session-token")

          if (cookie) return cookie
          else return ""
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return encode(params)
      },
      async decode(params) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          return null
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return decode(params)
      },
    },
    adapter,
    providers: [
      CredentialsProvider({
        name: "Credentials",
        credentials: {
          email: {},
          name: {},
          password: {},
        },
        async authorize(credentials, req) {
          if (!credentials) return null

          const { name, email, password } = credentials

          let user: User | null = null

          if (name && email && password) {
            user = await prisma.user.create({
              data: {
                name,
                email,
                password: await bcrypt.hash(password, 10),
              },
            })
          } else {
            user = await prisma.user.findUnique({
              where: {
                email,
              },
            })

            if (!user) return null
            if (!user.password) return null
            if (!bcrypt.compareSync(password, user.password)) {
              return null
            }
          }

          return {
            id: user.id,
            name: user.name,
            email: user.email,
            image: user.image,
          }
        },
      }),
      GoogleProvider({
        clientId: env.GOOGLE_CLIENT_ID,
        clientSecret: env.GOOGLE_CLIENT_SECRET,
      }),
    ],
    pages: {
      signIn: "/account/signin",
    },
    session,
  }
}

export default async function auth(req: NextApiRequest, res: NextApiResponse) {
  // Do whatever you want here, before the request is passed down to `NextAuth`
  console.log(
    "AUTH",
    (process.env as any).NEXTAUTH_URL,
    env.NEXTAUTH_URL,
    (process.env as any).VERCEL_URL
  )
  return await NextAuth(req, res, authOptions(req, res))
}

but for some reason the logs in the callbacks and adapter are never reached when fetching the session. The login instead works fine.
I've no clue of what's wrong..

@mattiaz9
Comment options

I found out what my problem was. Apparently when you login with the default form next-auth changes the cookie from next-auth.session-token to __Secure-next-auth.session-token in production.
If instead you use the function signIn in a custom form it doesn't, so the session works in a local environment but not in production.

I'm not sure if this is a bug or intended, but manually setting the session cookie name fixed the issue:

  ...
  cookies: {
    sessionToken: {
      name: "next-auth.session-token",
      options: {
        httpOnly: true,
        sameSite: "lax",
        path: "/",
        secure: env.NODE_ENV === "production",
      },
    },
  },
  ...
@klinsc
Comment options

@mattiaz9 I face the same issue, your solution helped me solved it.

I think this is nor a bug or intended, since we're doing the customization from the start. Cheers!

@Mahmaddz
Comment options

See my repo, I've succesfully deployed to vercel, I am using postgresql as db

It's working here: https://nextauth.abehidek.me

you save me

@DeNice-r
Comment options

@abehidek God bless you and your family 🙏 🙏 🙏 I will pray for you in my local church.

Comment options

The described workaround really works great. Thanks a lot for that. I now try to figure out, why it works, but I have quite a hard time to wrap my head around the next-auth source code. The workaround seems to force that getSessionAndUser is called even for a credential provider. Is this correct? How does it do that? By returning a string in JWTs encode, or null in decode?

You must be logged in to vote
3 replies
@engrusmanbelloa
Comment options

The described workaround really works great. Thanks a lot for that. I now try to figure out, why it works, but I have quite a hard time to wrap my head around the next-auth source code. The workaround seems to force that getSessionAndUser is called even for a credential provider. Is this correct? How does it do that? By returning a string in JWTs encode, or null in decode?

i am using using mongobd adapter, the adapter create the session in the db but the entire session async function block seems to be skipped there by not returning session to the client which takes me back to the initial issue dont know ho to wrap around it.

import NextAuth from "next-auth"
import { MongoDBAdapter } from "@next-auth/mongodb-adapter"
import clientPromise from "../auth/lib/mongodb"
import { MongoClient } from 'mongodb';
import bcrypt from 'bcrypt'
import GoogleProvider from "next-auth/providers/google";
import FacebookProvider from "next-auth/providers/facebook";
import CredentialsProvider from "next-auth/providers/credentials";

// Modules needed to support key generation, token encryption, and HTTP cookie manipulation 
import { randomUUID } from 'crypto'
// import Cookies from 'cookies'
import { setCookie, getCookie } from 'cookies-next';
import { encode, decode } from 'next-auth/jwt'



const MONGODB_URI = process.env.MONGODB_URI
// next auth starts here

export default async function handler(req, res) {
  const adapter = MongoDBAdapter(clientPromise)
  let userAccount = null
  // Do whatever you want here, before the request is passed down to `NextAuth`
  const generate = {}
  generate.uuid = function () {
    return uuidv4()
  }

  generate.uuidv4 = function () {
    return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
        (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
    )
  }
// Helper functions to generate unique keys and calculate the expiry dates for session cookies
  const generateSessionToken = () => {
    // Use `randomUUID` if available. (Node 15.6++)
    return randomUUID?.() ?? generate.uuid()
  }

  const fromDate = (time, date = Date.now()) => {
    return new Date(date + time * 1000)
  }

  //    callbacks for the sessions
const callbacks = {
  async redirect({ url, baseUrl }) {
    // Allows relative callback URLs
    if (url.startsWith("/")) return `${baseUrl}${url}`
    // Allows callback URLs on the same origin
    else if (new URL(url).origin === baseUrl) return url
    return baseUrl
  },
  async signIn({ user, account, profile, email, credentials }) {
    console.log("User Signin Start: ", user)
    // Check if this sign in callback is being called in the credentials authentication flow. If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
    if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
      if (user) {
          const sessionToken =  generateSessionToken()
          const sessionMaxAge = 60 * 60 * 24 * 7; //7Days
          const sessionExpiry = fromDate(sessionMaxAge)

          console.log("Session token is: ", sessionToken)
           console.log("sessionExpiry: ", sessionExpiry);
          
          await adapter.createSession({
              sessionToken: sessionToken,
              userId: user.username,
              expires: sessionExpiry
          })
          
          setCookie("next-auth.session-token", sessionToken, {
            expires: sessionExpiry,
            req: req,
            res: res,
          })
          console.log("user Session: ", user)
         
      }   
  }
    return true
  },

  async jwt({ token, user, account, profile, isNewUser }) {
    console.log("JWT callback. Got User: ", user)
    if (typeof user !== typeof undefined) {
      token.user = user;
    }
    return token
  },

  async session({ session, token}) {

    console.log("Session. Got User: ", session, token)
    if (userAccount !== null) {
      console.log("UserAccount Session Generation: ", user)
      session.user = {
        name: userAccount.name,
        email: userAccount.email,
      };
    console.log("Session.user: ", session.user)
    // return session
  }
  if (
    token && typeof token.user !== typeof undefined && (typeof session.user === typeof undefined ||
      (typeof session.user !== typeof undefined && typeof session.user.id === typeof undefined))
  ) {
    session.user = token.user
  }
  if (typeof token !== typeof undefined) {
    session.token = token
  }
  console.log("Session: ", session)
  return session
},
}

  const options = {
      session: {
              strategy: "database",
              maxAge: 7 * 24 * 60 * 60,
              updateAge: 24 * 60 * 60,
              // generateSessionToken: () => {
              //   return randomUUID?.() ?? randomBytes(32).toString("hex")
              // }    
            },
      jwt: {
        // Customize the JWT encode and decode functions to overwrite the default behaviour of storing the JWT token in the session cookie when using credentials providers. Instead we will store the session token reference to the session in the database.
        encode: async (token, secret, maxAge) => {
            if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
                // const cookies = new Cookies(req,res)

                // const cookie = cookies.get('next-auth.session-token')
                const cookie = getCookie("next-auth.session-token", { req: req });

                console.log("pure Cookie: ", cookie);

                if(cookie) return cookie
                else return ''

            }
            // Revert to default behaviour when not in the credentials provider callback flow
            return encode(token, secret, maxAge)
        },
        decode: async (token, secret) => {
            if (req.query.nextauth.includes('callback') && req.query.nextauth.includes('credentials') && req.method === 'POST') {
                return null
            }

            // Revert to default behaviour when not in the credentials provider callback flow
            return decode(token, secret)
        }
      },
      debug: process.env.NODE_ENV === "development",
      adapter,
      secret: process.env.NEXTAUTH_SECRET,
      providers: [

        CredentialsProvider({
          name: 'Credentials',
          async authorize(credentials, req) {
            const client = new MongoClient(MONGODB_URI, {},) 
            try {
                await client.connect()
            } catch (e) {
                console.error(e)
            }
            const email = credentials.email
            const password = credentials.password
            //Get all the users
            const users = client.db().collection('users')
            //Find user with the email  
            const user = await users.findOne(
              {
                  $or: [
                      { email: email }, { username: email }
                  ]
              }
            )

          //   //Not found - send error res
            if (!user) {
                client.close();
                throw new Error('No user found with this credential')
            }

            console.log("Authorize User Credentials: ", user)
            //Check hased password with DB password
            const checkPassword = bcrypt.compareSync(password, user.password)
            //Incorrect password - send response
            if (!checkPassword) {
                client.close();
                throw new Error('Password doesnt match')
            }
            //Incorrect password - send response
            if (!checkPassword) {
                client.close();
                throw new Error('Password doesnt match')
            }
            //Else send success response
            userAccount = {
              id: user._id,
              name: user.name,
              email: user.email,
            };
            client.close();
            return userAccount;
        },
          
        }),
        GoogleProvider({
        clientId: process.env.GOOGLE_CLIENT_ID,
        clientSecret: process.env.GOOGLE_CLIENT_SECRET,
        allowDangerousEmailAccountLinking: true,
        }),
        FacebookProvider({
            clientId: process.env.FACEBOOK_CLIENT_ID,
            clientSecret: process.env.FACEBOOK_CLIENT_SECRET
        }),
      // ...add more providers here
    ],
    callbacks: callbacks,
    pages: {
      signIn: '/signin',
      signOut: '/signout',
      error: '/auth/error', // Error code passed in query string as ?error=
      verifyRequest: '/auth/verify-request', // (used for check email message)
      // newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
    }, 
    
  }

  return await NextAuth(req, res, options)

}
@engrusmanbelloa
Comment options

on getting this one working i will working session deletion on user sihn out but this is really giving me headache

@thomassleeman
Comment options

Did you ever get this working? I have the exact same issue.

Comment options

If, like me, your motivation for wanting this is to be able to revoke user's sessions on logout, I found a solution.

  1. Continue using JWT.
  2. In CredentialsProvider authorize, generate a uuid and attach to jwt.
  3. Save this uuid in DB against user.
  4. On signOut event, get uuid from jwt + delete uuid from DB.
  5. On session callback, get uuid from jwt + check uuid exists in DB against user.
You must be logged in to vote
1 reply
@enyelsequeira
Comment options

do you have an example of this?

Comment options

hey @Ahmadh26 I am wondering if this still works? I am following the example you provided and could not make it work, I keep getting these errors
image
and my code, is the exact same as yours

[...nextauth].ts

// hanlder
export const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  console.log({ req, res });
  const data = requestWrapper(req, res);
  return await NextAuth(...data);
};

export function requestWrapper(
  req: NextApiRequest,
  res: NextApiResponse
): [req: NextApiRequest, res: NextApiResponse, opts: NextAuthOptions] {
  const adapter = PrismaAdapter(prisma);

  const generateSessionToken = () => randomUUID();
  const fromDate = (time: number, date = Date.now()) =>
    new Date(date + time * 1000);

  const opts: NextAuthOptions = {
    adapter: adapter,
    callbacks: {
      session({ session, user }) {
        if (session.user) {
          session.user.id = user.id;
        }
        return session;
      },
      async signIn({ user, account, profile, email, credentials }) {
        console.log("SSINGGGGGGGGG");
        // Check if this sign in callback is being called in the credentials authentication flow. If so, use the next-auth adapter to create a session entry in the database (SignIn is called after authorize so we can safely assume the user is valid and already authenticated).
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          if (user) {
            const sessionToken = generateSessionToken();
            const sessionMaxAge = 60 * 60 * 24 * 30; //30Daysconst sessionMaxAge = 60 * 60 * 24 * 30; //30Days
            const sessionExpiry = fromDate(sessionMaxAge);

            await adapter.createSession({
              sessionToken: sessionToken,
              userId: user.id,
              expires: sessionExpiry,
            });

            const cookies = new Cookies(req, res);

            cookies.set("next-auth.session-token", sessionToken, {
              expires: sessionExpiry,
            });
          }
        }

        return true;
      },
    },
    jwt: {
      encode: async ({ token, secret, maxAge }) => {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth.includes("credentials") &&
          req.method === "POST"
        ) {
          const cookies = new Cookies(req, res);
          const cookie = cookies.get("next-auth.session-token");
          if (cookie) return cookie;
          else return "";
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return encode({ token, secret, maxAge });
      },
      decode: async ({ token, secret }) => {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth.includes("credentials") &&
          req.method === "POST"
        ) {
          return null;
        }

        // Revert to default behaviour when not in the credentials provider callback flow
        return decode({ token, secret });
      },
    },
    secret: env.NEXTAUTH_SECRET,

    providers: [
      DiscordProvider({
        clientId: env.DISCORD_CLIENT_ID,
        clientSecret: env.DISCORD_CLIENT_SECRET,
        allowDangerousEmailAccountLinking: true,
      }),
      GitHubProvider({
        clientId: env.GITHUB_CLIENT_ID,
        clientSecret: env.GITHUB_CLIENT_SECRET,
        allowDangerousEmailAccountLinking: true,
      }),
    ],
  };

  return [req, res, opts];
}

and then in my get-server-auth-session i have this

import { requestWrapper } from "@/pages/api/auth/[...nextauth]";
import type { NextApiRequest, NextApiResponse } from "next";
import { unstable_getServerSession } from "next-auth";

// Next API route example - /pages/api/restricted.ts
export const getServerAuthSession = async (ctx: {
  // req: GetServerSidePropsContext["req"];
  // res: GetServerSidePropsContext["res"];
  req: NextApiRequest;
  res: NextApiResponse;
}) => {
  return await unstable_getServerSession(...requestWrapper(ctx.req, ctx.res));
};

Right now, its not working with those provider, but also doesn't work with credential provider

You must be logged in to vote
1 reply
@Ahmadh26
Comment options

honestly not sure what's wrong with your code.
nextauth is returning status code 500, which means something is not right within your code and there could be an unhandled exception error.

try to debug your code and see what is causing the status 500.

i followed @nneko's comment on how to switch to database strategy and it helped a lot
#4394 (reply in thread)

here is his blogpost:
https://branche.online/next-auth-credentials-provider-with-the-database-session-strategy/

this helped a lot with fixing my issues.

Comment options

I was able to use database strategy with credentials provider and everything else works perfectly.
Thanks to @nneko

here is the comment he provided:
#4394 (reply in thread)

and his blogpost about the issue:
https://nneko.branche.online/next-auth-credentials-provider-with-the-database-session-strategy/

You must be logged in to vote
7 replies
@edw19
Comment options

@Ahmadh26 i confirm

@nneko
Comment options

@Ahmadh26 can you update the answer to point to https://nneko.branche.online/next-auth-credentials-provider-with-the-database-session-strategy/ as I had to move the blog from the root domain at branche.online and it is now at nneko.branche.online.

@Ahmadh26
Comment options

@nneko Done 👍

@proccedure-caze
Comment options

Guys any update on that in next 14?

@begalinsaf
Comment options

@proccedure-caze same,i still waiting for that

Answer selected by Ahmadh26
Comment options

For someone who needs a complete code using next-auth Credentials + session + DB adapter, here's the final [...nextauth].js file.
I slightly modified @mattiaz9 's answer for MongoDB users.

import NextAuth from "next-auth"
import { encode, decode } from "next-auth/jwt"
import CredentialsProvider from "next-auth/providers/credentials"
import bcrypt from "bcryptjs"
import Cookies from "cookies"
import { randomUUID } from "crypto"
import { MongoDBAdapter } from "@next-auth/mongodb-adapter";
import { connectDB } from "@/util/database";

const getAdapter = (req, res)=> ({
  ...MongoDBAdapter(connectDB),
  async getSessionAndUser(sessionToken) {
    let db = (await connectDB).db('YOURDBNAME');
    const userAndSession = await db.collection('sessions').findOne({
      sessionToken : sessionToken
    })
    console.log("SESSION USER :", sessionToken, userAndSession)
    if (!userAndSession) return null
    
    //insert session data whatever you like 
    const { user, ...session } = userAndSession
    console.log("USER", user)
    return { user: user, session : session }
  },
})

const session = {
  // strategy: "database",
  maxAge: 30 * 24 * 60 * 60, // 30 days
  updateAge: 24 * 60 * 60, // 24 hours
}

export const authOptions = (req, res) => {
  const adapter = getAdapter(req, res)
  return {
    providers: [
      CredentialsProvider({
        name: "Credentials",
        credentials: {
          email: { label: "email", type: "text" },
          password: { label: "password", type: "password" },
        },
        
        async authorize(credentials, req) {
          try {
            let client = (await connectDB).db('YOURDBNAME');
            const user = await client.collection('USERCOLLECTION').findOne({email: credentials.email})
            console.log("Authorize User Credentials: ", user);
            if (user !== null) {
              const res = await bcrypt.compare(credentials.password,user.password)
              if (res === true) {
                let userAccount = {
                  id: user._id.toString(),
                  name : user.username,  //name & email properties are required (strange)
                  email: user.email,
                };
                console.log("UserAccount created: ", userAccount);
                return userAccount;
              } else {
                console.log("Wrong password");
                return null;
              }
            } else {
              return null;
            }
          } catch (err) {
            console.log("authorize error :", err);
          }
        },
      }),
    ],
    
    
    callbacks: {
      session({ session, user }) {
        console.log("SESSION callback", session, user)
        if (session.user) {
          session.user.id = user.id
        }
        return session
      },
      async signIn({ user }) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          if (user && "id" in user) {
            const sessionToken = randomUUID()
            const sessionExpiry = new Date(Date.now() + session.maxAge * 1000)
            await adapter.createSession({
              sessionToken : sessionToken,
              userId: user.id,
              user : {
                name : user.name,
                email : user.email
              },
              expires: sessionExpiry,
              // userAgent: req.headers["user-agent"] ?? null,
            })
            const cookies = new Cookies(req, res)
            cookies.set("next-auth.session-token", sessionToken, {
              expires: sessionExpiry,
            })
          }
        }
        return true
      },
    },
    
    
    //needs to override default jwt behavior when using Credentials 
    jwt: {
      encode(params) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          const cookies = new Cookies(req, res)
          const cookie = cookies.get("next-auth.session-token")
          if (cookie) return cookie
          else return ""
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return encode(params)
      },
      async decode(params) {
        if (
          req.query.nextauth?.includes("callback") &&
          req.query.nextauth?.includes("credentials") &&
          req.method === "POST"
        ) {
          return null
        }
        // Revert to default behaviour when not in the credentials provider callback flow
        return decode(params)
      },
    },
    adapter,
    session,
  }
}
export default async function auth(req, res) {
  // Do whatever you want here, before the request is passed down to `NextAuth`
  return await NextAuth(req, res, authOptions(req, res))
}
  • Works in Next.js 13 / next-auth 4.19.2
  • 'USERCOLLECTION' documents need registered user's information, at least name, email, _id, password properties.
  • User's session data successfully appears in client components when using useSession() hook
  • Only tested locally
You must be logged in to vote
3 replies
@codingapple1
Comment options

I recently checked and getServerSession() doesn't show user's session data in server components and API routes. 😢
It shows JWEInvalid: Invalid Compact JWE error.

@tconroy
Comment options

Also having this Invalid Compact JWE error @codingapple1 , did you find a solution?

@whispernight
Comment options

me too, wtf, any fixes?

Comment options

So I have to handle the session creation myself if I use credentials to log in?

You must be logged in to vote
1 reply
@Ahmadh26
Comment options

yes

Comment options

I've created the workaround for using NextAuth with a custom database and credentials and it works for when I want to useSession client side, however I'm stuck on when I want to use getServerSession as I need to pass the req and res which doesn't match for serverSideProps and what [...nextauth] is expecting. Has anyone been able to make this work with getServerSession?

Any ideas?

login.tsx

export async function getServerSideProps(context: GetServerSidePropsContext) {
  const session = await getServerSession(context.req, context.res, authOptions);
  ...
  }

Type '(req: NextApiRequest, res: NextApiResponse) => Promise' has no properties in common with type 'GetServerSessionOptions'.

You must be logged in to vote
6 replies
@hmar13
Comment options

@Ahmadh26 made this change but doesn't seem to work either.

The problem I've got is that in my [...nextauth].ts file I'm exporting the handler which requires the request and response for checking if the request query has a callback and credentials, as described in the solutions above.

Even if I move the authOptions out of the handler and export/import it separately , it will still need the request and response as arguments.

Are you using getServerSession in your app? If so, do you mind sharing how?

Example:

  const session = await getServerSession(context.req, context.res, authOptions(req, res)
@Ahmadh26
Comment options

@hmar13 i'll be more than happy to help out with the issue itself. if you'd like we can take this on discord and i can probably share some code snippets there if you're okay with it?

edit: my discord is in my bio if you're interested.

@hmar13
Comment options

Will do, thanks!

@Kartik4152
Comment options

@hmar13 were you able to figure this out? facing the same issue.

@hmar13
Comment options

@Kartik4152 I gave it a few more try's but couldn't make it work, sorry. This on top of other problems that kept arising with NextAuth put me off the whole thing tbh. Just using iron-session now and did the OAuth myself.

Comment options

The latest next auth has ways to allow useSession to work with JWT strategy without doing workaround to use database strategy

Here is my auth config for next auth and it worked (I can use useSession as if it is database when it is jwt instead)

import { type GetServerSidePropsContext } from "next";
import {
  getServerSession,
  type NextAuthOptions,
  type DefaultSession,
} from "next-auth";
import DiscordProvider from "next-auth/providers/discord";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { env } from "~/env.mjs";
import { prisma } from "~/server/db";

/**
 * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
 * object and keep type safety.
 *
 * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
 */
declare module "next-auth" {
  interface Session extends DefaultSession {
    user: {
      id: string;
      // ...other properties
      // role: UserRole;
    } & DefaultSession["user"];
  }

  // interface User {
  //   // ...other properties
  //   // role: UserRole;
  // }
}

/**
 * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
 *
 * @see https://next-auth.js.org/configuration/options
 */
export const authOptions: NextAuthOptions = {
  callbacks: {
    async jwt({ token, account, profile }) {
      // Persist the OAuth access_token and or the user id to the token right after signin
      console.log('jwt callback', token, account, profile)
      if (account) {
        token.id = account.id
      }
      return token
    },
    session({ session, token, user }) {
      console.log('session callback', session, token, user)
      if (session.user) {
        session.user.id = user?.id || token?.sub;
      }
      return session;
    },
  },
  adapter: PrismaAdapter(prisma),
  providers: [
    //DiscordProvider({
      //clientId: env.DISCORD_CLIENT_ID,
      //clientSecret: env.DISCORD_CLIENT_SECRET,
    //}),
    CredentialsProvider({
      id: "phone-credentials",
      name: "Two Factor Auth",
      async authorize(credentials, req) {
        console.log("authorize", credentials)
        let user;
        user = await prisma.user.findUnique({
          where: {
            phone: credentials?.phone
          }
        })
        if (!user) {
          user = await prisma.user.create({
            data: {
              phone: credentials?.phone,
              phoneVerified: new Date(),
            }
          })
        }
        return user
      },
      credentials: {
        phone: { label: "Phone", type: "text ", placeholder: "+12479120856" },
      },
    }),
    /**
     * ...add more providers here.
     *
     * Most other providers require a bit more work than the Discord provider. For example, the
     * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
     * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
     *
     * @see https://next-auth.js.org/providers/github
     */
  ],
  session: {
    strategy: "jwt", // for credentials
  },
  pages: {
    signIn: "/" // this will allow us to use our own login page
  }
};

/**
 * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
 *
 * @see https://next-auth.js.org/configuration/nextjs
 */
export const getServerAuthSession = (ctx: {
  req: GetServerSidePropsContext["req"];
  res: GetServerSidePropsContext["res"];
}) => {
  return getServerSession(ctx.req, ctx.res, authOptions);
};
You must be logged in to vote
0 replies
Comment options

Next.js + Prisma + NextAuth with Session

This repository combines Next.js and NextAuth.js with various authentication providers (Credentials, Facebook, Google) and session management for both App and Pages Routers.

You can find the source code for this project on GitHub and explore the live site here.

NextAuth handler + Sign up handler (v4)

✨ NextAuth setup + Signup action (v5)

v5 simple setup

Feel free to explore the codebase and the live site to gain insights into the implementation details. Enjoy your journey with Next.js, Prisma, and NextAuth.js!

You must be logged in to vote
17 replies
@solo-samurai
Comment options

@tydolla00 you won't get signIn errors in the catch block. you need to await the promise and check if there's an error

const res = await signIn("credentials", {
  ...data,
  redirect: false,
});

if (res?.error) {
  toast({
    variant: "destructive",
    description: res.error,
  });
}
@solo-samurai
Comment options

@glendell03 maybe you're missing some envs, maybe this could help debug: process.env.NODE_ENV === "development" X debug: true.
I will link a repo and host it on Vercel in the coming days.

@glendell03
Comment options

@decovicdev I got this error
on localhost It does have session but on the deployed version no session

Localhost
image
Deployed
image

@solo-samurai
Comment options

@glendell03 that error has been posted by another user, I have updated the solution since.

encode: async (arg) => {
          if (isCredentialsCallback) {
            const cookie = cookies().get("next-auth.session-token");

            if (cookie) return cookie.value;
            return "";
          }

          return encode(arg);
        },

the cookie is an object, make sure to return the cookie values in the encode function from next-auth option.

@OahMada
Comment options

I hope there is more explanation regarding what the authorization flow is according to this setting, what the purpose of the JWT callback in the auth config is, because we are using a database session instead of a JWT token? Mainly, it's the JWT callback that I find difficult to wrap my head around. Or are there any reference materials I can look into?

Update: I found this article that explains a lot. https://clerk.com/blog/combining-the-benefits-of-session-tokens-and-jwts
Maybe the next step is to renew JWT tokens in the middleware or elsewhere?

Comment options

Hello @Ahmadh26, I understand that this was a few months ago, but I am now developing an app that will use NextAuth that uses database sessions using a credential provider. Do you mind if you can help me out over on Discord?

You must be logged in to vote
6 replies
@EyadElghobary
Comment options

Sent you a friend request!

@TommyLeong
Comment options

sent you a request too! :)

@Ahmadh26
Comment options

Send another one. I think I ignored it by mistake. :) @TommyLeong

@TommyLeong
Comment options

No worries, sent! :)

@kartikgajjar7
Comment options

Sure, I can try :) My username is jayy26

hey can you help me in discord too?

Comment options

Hi all, I come up with another way to solve the problem. Instead of persist credential info in database, I give different NextAuthOptions based on the request instead. Any comments?

export function isCredentialsCallback(req: NextApiRequest) {
  return (
    req.query.nextauth?.includes("callback") &&
    req.query.nextauth?.includes("credentials") &&
    req.method === "POST"
  );
}

export const getAuthOptions = (req: NextApiRequest) => {
  if (isCredentialsCallback(req)) {
    return credentialsOptions;
  } else {
    return authOptions;
  }
};

/**
 * Wrapper for `getServerSession` so that you don't need to import the `authOptions` in every file.
 *
 * @see https://next-auth.js.org/configuration/nextjs
 */
export const getServerAuthSession = (ctx: {
  req: NextApiRequest;
  res: NextApiResponse;
}) => {
    return getServerSession(ctx.req, ctx.res, getAuthOptions(ctx.req));
};

export default async function authHandler(req: NextApiRequest, res: NextApiResponse) {
  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  return await NextAuth(req, res, getAuthOptions(req));
}
You must be logged in to vote
3 replies
@devdatkumar
Comment options

anyone tested this? :D

@devdatkumar
Comment options

@Firerer, could you please provide link to full code, this doesn't seems to be working for me.

also, I have merged first two functions on one:

export const getAuthOptions = (req: NextApiRequest) => {
  if (
    req.query.nextauth?.includes("callback") &&
    req.query.nextauth?.includes("credentials") &&
    req.method === "POST"
  ) {
    return credentialsOptions;
  }
  return authOptions;
};
@devdatkumar
Comment options

my code:

\src\app\api\auth[...nextauth]\authOptions.ts

import { NextAuthOptions } from "next-auth";
import GithubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";

import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "../../../../db/index";
import { eq } from "drizzle-orm";
import { users } from "@/db/schema/schema";

export const authOptions: NextAuthOptions = {
  adapter: DrizzleAdapter(db),
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    GithubProvider({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
};

export const credentialsOptions: NextAuthOptions = {
  secret: process.env.NEXTAUTH_SECRET,
  providers: [
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: {
          label: "Email",
          type: "email",
          placeholder: "Enter Email",
        },
        password: {
          label: "Password",
          type: "password",
          placeholder: "Enter Password",
        },
      },
      async authorize(credentials) {
        try {
          const [user] = await db
            .select()
            .from(users)
            .where(eq(users.email, credentials!.email));

          if (user && credentials?.password === "alphapass") {
            // Replace the password check with proper password hashing and validation
            return user;
          }
        } catch (error) {
          console.error("Authentication error:", error);
        }
        return null;
      },
    }),
  ],
};

\src\app\api\auth[...nextauth]\authOptions.ts

import { authOptions, credentialsOptions } from "./authOptions";
import { NextApiRequest, NextApiResponse } from "next";

export const getAuthOptions = (req: NextApiRequest) => {
  if (
    req.query.nextauth?.includes("callback") &&
    req.query.nextauth?.includes("credentials") &&
    req.method === "POST"
  ) {
    return credentialsOptions;
  }
  return authOptions;
};

export const getServerAuthSession = (ctx: {
  req: NextApiRequest;
  res: NextApiResponse;
}) => {
  return getServerSession(ctx.req, ctx.res, getAuthOptions(ctx.req));
};

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  return await NextAuth(req, res, getAuthOptions(req));
};

export { handler as GET, handler as POST };
Comment options

I agree that passwords are bad (and that we should only trust password authentication to huge, established, corporations \s), but I have an existing table of users with emails and passwords that I need to accommodate. So...

Here is a solution to JWT & Database Sessions using the email/password CredentialsProvider, that:

  • uses a JWT for authentication
  • creates a database session when signing in
  • deletes the database session when signing out
  • signs out the user if the database session is removed
import { randomUUID } from "crypto";
import type { DefaultSession } from "@auth/core/types";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { Prisma } from "@prisma/client";
import bcrypt from "bcrypt";
import NextAuth from "next-auth";
import type { Session, SessionStrategy, TokenSet } from "next-auth/core/types";
import CredentialsProvider from "next-auth/providers/credentials";
import isEmail from "validator/lib/isEmail";

import { prisma } from "@acme/db";

export type { Session } from "next-auth";

export const providers = ["email", "discord"] as const;
export type OAuthProviders = (typeof providers)[number];

const client = PrismaAdapter(prisma);

declare module "next-auth" {
  interface Session {
    user: {
      id: string;
    } & DefaultSession["user"];
  }
}

const maxAge = 30 * 24 * 60 * 60; // 30 days

const authorize = async (credentials) => {
  const { email, password } = credentials;
  let user;

  try {
    if (!isEmail(email)) {
      throw new Error("Email should be a valid email address");
    }
    user = await client.getUserByEmail(email);
    if (!user) {
      user = await client.createUser({
        email,
        password: bcrypt.hashSync(password, 10),
      });
    } else {
      const passwordsMatch = await bcrypt.compare(password, user.password);
      if (!passwordsMatch) {
        throw new Error("Password is not correct");
      }
    }
    const token = randomUUID();
    await client.createSession({
      userId: user.id,
      expires: new Date(Date.now() + maxAge * 1000),
      sessionToken: token,
    });
    return {
      id: user.id,
      email: user.email,
      name: user.name,
      image: user.image,
      sessionToken: token,
    };
  } catch (error) {
    console.error(error.message);
    throw error;
  }
};

const EmailCredentials = CredentialsProvider({
  name: "email",
  credentials: {
    email: { label: "Email", type: "text" },
    password: { label: "Password", type: "password" },
  },
  authorize,
});

export const authOptions = {
  secret: process.env.NEXTAUTH_SECRET,
  adapter: client,
  providers: [
    EmailCredentials
  ],
  session: {
    strategy: "jwt" as SessionStrategy,
    maxAge: maxAge, // 30 days
    updateAge: 24 * 60 * 60, // 24 hours
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) {
        token.userId = user.id;
        token.sessionToken = user.sessionToken;
      }
      if (token?.sessionToken) {
        const { session } = await client.getSessionAndUser(token.sessionToken);
        if (!session) {
          return null;
        }
      }
      return token;
    }
  },
  events: {
    signOut: async ({ token, session }) => {
      if (token?.sessionToken) {
        await client.deleteSession(token.sessionToken);
      }
    },
  },
};

export const {
  handlers: { GET, POST },
  auth,
  CSRF_experimental,
} = NextAuth(authOptions);

The authorize method validates the email, hashes the password with bcrypt, compares password hashes, creates a database session, and returns the user object and sessionToken.

The jwt callback is used to add the sessionToken to the token object used to generate the JWT, tying the JWT to the database session, and validates the current JWT token against the database.

A signOut event is used delete the session from the database if the sessionToken is present in the JWT.

Thanks @programmarchy for the suggestions!

Tested and tested against create-t3-turbo. An example repo is at tyrauber/create-t3-turbo/tree/feat/credentialsAuthentication. And t3-oss/create-t3-turbo PR #469.

You must be logged in to vote
1 reply
@AveshLutchman
Comment options

Tried this out but got a lot of TS complaints.

Ignoring those warnings, I also got errors where GET and POST are undefined (I'm still on next-auth and haven't gotten @auth yet, so maybe that's the issue). Also got some complaints about 'id' not existing.

I'll attempt again in a bit however, just putting my comment out there in case someone else is in the same spot or has solved these issues.

Comment options

Can Anyone provide a suitable SignUp code for this approach?
I am kinda stuck on the Accounts part how it will work

You must be logged in to vote
3 replies
@ted-dino
Comment options

did you find a solution? im also doing authentication and looking for a solution when signing up

@valeriusec
Comment options

@ted-dino The solution that @decovicdev provided with some changes it works for me, I'll send you the complete code.

/[...nextauth]/route.ts

import NextAuth, { AuthOptions } from "next-auth";
import { decode, encode } from "next-auth/jwt";
import { Prisma } from "@prisma/client";
import { cookies } from "next/headers";
import { randomUUID } from "node:crypto";
import bcryptjs from "bcryptjs";
import prisma from "@/lib/database/prisma";
import { PrismaAdapter } from "@next-auth/prisma-adapter";
import { NextRequest } from "next/server";
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
import { ENV } from "@/config/env-config";

interface Context {
  params: { nextauth: string[] };
}

// Configuration wrapper for NextAuth
const authOptionsWrapper = (request: NextRequest, context: Context) => {
  const { params } = context;

  // Determine if the current request is related to credentials callback
  const isCredentialsCallback =
    params?.nextauth?.includes("callback") &&
    params.nextauth.includes("credentials") &&
    request.method === "POST";

  // Common JWT options shared between encode and decode
  const commonJwtOptions: AuthOptions["jwt"] = {
    maxAge: 60 * 60 * 24 * 30,
    encode: async (arg) => {
      if (isCredentialsCallback) {
        // Retrieve and return session token from the cookie
        const cookie = cookies().get("__Secure-next-auth.session-token");
        return cookie?.value || "";
      }
      return encode(arg);
    },
    decode: async (arg) => {
      if (isCredentialsCallback) {
        // Prevent decoding during credentials callback
        return null;
      }
      return decode(arg);
    },
  };

  return [
    request,
    context,
    {
      // Prisma Adapter for session and user management
      adapter: PrismaAdapter(prisma),
      providers: [
        // Google Authentication Provider
        GoogleProvider({
          clientId: ENV.GOOGLE_ID!,
          clientSecret: ENV.GOOGLE_SECRET!,
        }),
        // Credentials Authentication Provider
        CredentialsProvider({
          name: "Credentials",
          credentials: {
            email: { label: "email", type: "text" },
            password: { label: "Password", type: "password" },
          },
          authorize: async (credentials) => {
            try {
              if (credentials) {
                const { email, password } = credentials;
                // Retrieve user and associated accounts from the database
                const user = await prisma.user.findUnique({
                  where: { email },
                  include: { accounts: true },
                });

                if (!user) {
                  // Handle missing user account
                  throw new Error("User account does not exist");
                }

                const account = user.accounts[0];
                if (account.provider !== "credentials") {
                  // Handle non-credentials provider
                  throw new Error(`Please sign in with ${account.provider}`);
                }

                // Compare passwords using bcrypt
                const passwordsMatch = await bcryptjs.compare(
                  password,
                  user.password
                );

                if (!passwordsMatch) {
                  // Handle incorrect password
                  throw new Error("Password is not correct");
                }

                return user;
              }
            } catch (error) {
              // Handle Prisma-related errors
              if (
                error instanceof Prisma.PrismaClientInitializationError ||
                error instanceof Prisma.PrismaClientKnownRequestError
              ) {
                // Handle Prisma initialization errors
                throw new Error("System error. Please contact support");
              }

              throw error;
            }
          },
        }),
      ],
      callbacks: {
        // Handle sign-in events
        async signIn({ user }) {
          if (isCredentialsCallback && user) {
            // Generate session token and set cookie
            const sessionToken = randomUUID();
            const sessionExpiry = new Date(
              Date.now() + 60 * 60 * 24 * 30 * 1000
            );

            await prisma.session.create({
              data: {
                sessionToken,
                userId: user.id,
                expires: sessionExpiry,
              },
            });

            cookies().set("__Secure-next-auth.session-token", sessionToken, {
              expires: sessionExpiry,
            });
          }
          return true;
        },
        async redirect({ baseUrl }) {
          return baseUrl;
        },
      },
      secret: ENV.NEXTAUTH_SECRET,
      jwt: commonJwtOptions,
      debug: process.env.NODE_ENV === "development",
      pages: {
        signIn: `${ENV.BASE_URL}/signin`,
        newUser: `${ENV.BASE_URL}/`,
        signOut: `${ENV.BASE_URL}/signin`,
      },
    } as AuthOptions,
  ] as const;
};

// Authentication handler using NextAuth
function handler(request: NextRequest, context: Context) {
  return NextAuth(...authOptionsWrapper(request, context));
}

// Export handler for GET and POST requests
export { handler as GET, handler as POST };

/credentials-signin/route.ts

! IMPORTANT: The name of the folder containing the api route must be different from /signin so something like /credentials-signin works.

import { NextRequest, NextResponse } from "next/server";
import { Prisma } from "@prisma/client";
import bcrypt from "bcryptjs";
import prisma from "@/lib/database/prisma";

async function handler(request: NextRequest) {
  try {
    const body = await request.json();
    console.log(body);

    if (!body.email.trim() || !body.password.trim()) {
      return NextResponse.json(
        {
          status: 400,
          errors: "Email and password cannot be empty or whitespace.",
        },
        { status: 200 }
      );
    }

    const user = await prisma.user.findUnique({ where: { email: body.email } });
    if (user && user.password) {
      const passwordMatch = bcrypt.compareSync(body.password, user.password);
      if (passwordMatch) {
        return NextResponse.json(
          { status: 200, message: "User Logged in successfully!" },
          { status: 200 }
        );
      } else {
        return NextResponse.json(
          {
            status: 400,
            errors: "Please check your password.",
          },
          { status: 200 }
        );
      }
    } else if (user && !user.password) {
      return NextResponse.json(
        {
          status: 400,
          errors: "No password found for this user.",
        },
        { status: 200 }
      );
    } else {
      return NextResponse.json(
        {
          status: 400,
          errors: "No user found with the provided email.",
        },
        { status: 200 }
      );
    }
  } catch (error) {
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      return NextResponse.json(
        {
          status: 500,
          errors: "A database error occurred.",
        },
        { status: 200 }
      );
    }

    // Handle other errors
    return NextResponse.json({ error }, { status: 500 });
  }
}

export { handler as GET, handler as POST };

/credentials-signup/route.ts

! IMPORTANT: The name of the folder containing the api route must be different from /signup so something like /credentials-signup works.

import { Prisma } from "@prisma/client";
import prisma from "@/lib/database/prisma";
import { NextRequest, NextResponse } from "next/server";
import bcrypt from "bcryptjs";

async function handler(request: NextRequest) {
  const body = await request.json();

  if (
    !body ||
    !body.firstName ||
    !body.lastName ||
    !body.email ||
    !body.password
  ) {
    return NextResponse.json(
      { status: 400, errors: "Invalid data" },
      { status: 200 }
    );
  }

  console.log(body);

  try {
    const userExists = await prisma.user.findUnique({
      where: {
        email: body.email,
      },
    });

    if (userExists) {
      return NextResponse.json(
        {
          status: 400,
          errors: "Email already exists.",
        },
        { status: 200 }
      );
    } else if (!userExists) {
      const hash = await bcrypt.hash(body.password, 10);
      const newUser = await prisma.user.create({
        data: {
          firstName: body.firstName,
          lastName: body.lastName,
          name: `${body.firstName} ${body.lastName}`,
          email: body.email,
          password: hash,
        },
      });

      if (!newUser) {
        return NextResponse.json(
          {
            status: 400,
            errors: "Unable to create user account.",
          },
          { status: 200 }
        );
      }

      const newAccount = await prisma.account.create({
        data: {
          userId: newUser.id,
          type: "credentials",
          provider: "credentials",
          providerAccountId: newUser.id,
        },
      });

      if (newUser && newAccount) {
        return NextResponse.json(
          { status: 200, msg: "User Created successfully!" },
          { status: 200 }
        );
      } else {
        return NextResponse.json(
          {
            status: 400,
            errors: "Unable to create user account.",
          },
          { status: 200 }
        );
      }
    }
  } catch (error) {
    return NextResponse.json({ error }, { status: 500 });
  }
}

export { handler as POST, handler as GET };
@ted-dino
Comment options

hey @valeriusec, thanks a lot! im gonna check try this out, you really saved me a lot of headaches. i really appreciate it.

Comment options

Thank you for your code. I only wonder what will happen if they decide to make out live even worse with their religious views and on each update change the credentials callback name and silently break all of the above solutions.

You must be logged in to vote
0 replies
Comment options

My only worry remains how these workarounds would integrate with V5 when it's out.
Doubtless that V5 is also gonna be a pain to work with CredentialsProvider as well.

You must be logged in to vote
0 replies
Comment options

my solution using drizzles new adapter + postgresql. I'm using credentials provider to do SIWE. Using next 13 app router

database is only called on signin, data is persisted in the JWT. Should be pretty cheap on the database. I dont like using 'as' assertions but I had to satisfy typescript

lib/auth.ts


import { DefaultSession, DefaultUser, NextAuthOptions } from "next-auth";
import { db } from "@/lib/drizzle";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import CredentialsProvider from "next-auth/providers/credentials";
import { SiweMessage } from "siwe";
import { getCsrfToken } from "next-auth/react";
import { users } from "@/db/schema";
import type { NextApiRequest, NextApiResponse } from "next";
import { InferSelectModel } from "drizzle-orm";
import { randomUUID } from "crypto";
import { DefaultJWT } from "next-auth/jwt";

declare module "next-auth" {
  interface User extends DefaultUser {
    walletAddress: string;
  }
  interface Session extends DefaultSession {
    user: InferSelectModel<typeof users>;
  }
}

declare module "next-auth/jwt" {
  interface JWT extends DefaultJWT {
    walletAddress: string;
  }
}

export const auth = (
  req: NextApiRequest,
  res: NextApiResponse
): NextAuthOptions =>
  ({
    adapter: DrizzleAdapter(db),
    callbacks: {
      async jwt({ account, token, user }) {
        if (account) {
          token.walletAddress = user.walletAddress;
        }
        return token;
      },
      session({ session, token, user }) {
        if (session.user) {
          session.user.walletAddress = token.walletAddress;
        }
        return session;
      },
    },
    session: {
      strategy: "jwt",
    },
    providers: [
      CredentialsProvider({
        name: "Ethereum",
        type: "credentials",
        credentials: {
          message: {
            label: "Message",
            type: "text",
            placeholder: "0x0",
          },
          signature: {
            label: "Signature",
            type: "text",
            placeholder: "0x0",
          },
        },

        authorize: async (credentials, req) => {
          try {
            const siwe = new SiweMessage(
              JSON.parse(
                (credentials?.message as string) ?? "{}"
              ) as Partial<SiweMessage>
            );

            const nonce = await getCsrfToken({
              req: { headers: req?.headers },
            });
            const { success, data } = await siwe.verify({
              signature: credentials?.signature || "",
              nonce,
            });

            if (!success) {
              return null;
            }

            const [userQuery] = await db
              .insert(users)
              .values({ walletAddress: data.address, id: randomUUID() })
              .onConflictDoUpdate({
                target: [users.walletAddress],
                set: {
                  walletAddress: data.address,
                },
              })
              .returning();
              console.log("userQuery ran")
            return userQuery;
          } catch (e) {
            console.log(e);
            return null;
          }
        },
      }),
    ],
    secret: process.env.NEXTAUTH_SECRET,
  } as NextAuthOptions);

src/app/api/auth/[...nextauth]/route.ts


import NextAuth from "next-auth/next";
import { auth } from "@/lib/auth";
import { NextApiRequest, NextApiResponse } from "next";

const Auth = (req: NextApiRequest, res: NextApiResponse) => {
  const authOpts = auth(req, res);

  const isDefaultSigninPage =
    req.method === "GET" && req?.query?.nextauth?.includes("signin");

  if (isDefaultSigninPage) {
    authOpts.providers.pop();
  }

  return NextAuth(req, res, authOpts);
};

export { Auth as GET, Auth as POST };

You must be logged in to vote
0 replies
Comment options

Hello I found a solution that works for me and wanted to help anyone that was stuck like I was. This solution uses Typescript and app router. You can store credentials in a db as well as use NextAuth providers. I'm going to assume you have your own api/register route setup and a login/register page.

NextJS 13.4 of September 2023

app/api/auth/[...nextauth]/route.ts

  • The user object is only persisted on the initial signin, so if you wish to add any properties to the token, you need to add a condition so it is not overwritten on consecutive calls.
  • Adapter and callbacks are optional if you don't need any additional logic.
import NextAuth from "next-auth/next";
import { NextAuthOptions } from "next-auth";
import config from "@/app/api/services/config";
import GoogleProvider from "next-auth/providers/google";
import GithubProvider from "next-auth/providers/github";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaClient } from "@prisma/client";
import { PrismaAdapter } from "@auth/prisma-adapter";
import bcrypt from "bcryptjs";

const prisma = new PrismaClient();

export const authOptions = {
  adapter: PrismaAdapter(prisma),
  callbacks: {
    async signIn(params) {
      return true;
    },
    async session({ session, token }) {
      if (session.user?.name) session.user.name = token.name;
      return session;
    },
    async jwt({ token, user }) {
      // * User only available on first run.
      let newUser = { ...user } as any;
      if (newUser.first_name && newUser.last_name)
        token.name = `${newUser.first_name} ${newUser.last_name}`;
      return token;
    },
  },
  providers: [
    GoogleProvider({
      clientId: config.GOOGLE_CLIENT_ID,
      clientSecret: config.GOOGLE_CLIENT_SECRET,
    }),
    GithubProvider({
      clientId: config.GITHUB_CLIENT_ID,
      clientSecret: config.GITHUB_CLIENT_SECRET,
    }),
    CredentialsProvider({
      name: "Credentials",
      credentials: {
        email: { label: "email", type: "text" },
        password: { label: "Password", type: "password" },
      },
      async authorize(credentials, req) {
        // Find user within database
        const user = await prisma.user.findUnique({
          where: { username: credentials?.email },
        });

        if (user) {
          if (user.provider !== "Credentials")
            throw new Error(`Please sign in with ${user.provider}`);

          const matchingPassword =
            user.password &&
            credentials?.password &&
            (await bcrypt.compare(credentials.password, user.password));

          if (!matchingPassword)
            throw new Error("Incorrect Username or Password");
          return user;
        }

        throw new Error("User does not exist");
      },
    }),
  ],
  secret: config.NEXTAUTH_SECRET,
  session: { strategy: "jwt" },
} as NextAuthOptions;

const handler = NextAuth(authOptions);

export { handler as GET, handler as POST };

app/login/page.tsx

status.error returns the error message thrown in the authorize callback if there is one. Check the condition to handle any errors. callbackUrl is whatever URL you want the user to redirect to after login.

export default function Login() {
...
const onSubmit = async (data: any) => {
    const status = await signIn("credentials", {
      email: data.username,
      password: data.password,
      redirect: false, // Stops redirect to error page.
      callbackUrl: "/",
    });
    console.log(status);
    if (status?.error) {
      toast({
        variant: "destructive",
        description: status?.error || "Unexpected error",
      });
      return;
    }
    router.push("/");
 };
return (
<>
...
    <button
      onClick={() => signIn("google", { callbackUrl: callbackUrl })}
      className="btn btn-ghost btn-outline btn-primary"
    >
      Sign in with Google
    </button>
    <button
      onClick={() => signIn("github", { callbackUrl: callbackUrl })}
      className="btn btn-ghost btn-outline btn-primary"
    >
      Sign in with Github
    </button>

If you want to authenticate certain routes based on some logic you can do so in a middleware.ts file in the root.

import { withAuth } from "next-auth/middleware";

export default withAuth(
  function middleware(req) {
    // * Only called if authorized passes
    console.log(req.nextauth.token);
  },
  {
    callbacks: {
      authorized: ({ req, token }) => {
        console.log({ req, token });
        if (token === null) return false;
        return true;
      },
    },
    pages: {
      signIn: "/login",
    },
  }
);

// Specify the routes that need to be authenticated
export const config = { matcher: ["/profile"] };
You must be logged in to vote
0 replies
Comment options

The solution for me to add EmailProvider and not use it at all, it disables the error.

You must be logged in to vote
2 replies
@DorienMay
Comment options

Are you saying that just by adding EmailProvider you can freely use Credentials provider along with Strategy set to Session and get away with it? Can you provide your /app/api/auth/[...nextauth]/route.ts file so that we can see details?

@taylor-lindores-reeves
Comment options

I would also like to know about this if pos @stephenasuncionDEV

Comment options

Does anyone know if encode is only used by credentials when sessions.strategy='database'?. I can't seem to find any other route that will call encode other than credentials callback when session strategy is set to database. That means you could simply set encode to return the session token.

I want to avoid the overhead of having to re-init the configuration for each incoming request as some solutions did above to intercept the request and response objects. It also means I don't have to hard code the 'callback', 'credentials' and 'authjs.session-token', as setting cookies is handled as intended by the package.

  session: {
      strategy: "database",
      maxAge: maxAge,
      updateAge: 24 * 60 * 60,
  },
  jwt: {
      async encode({token}) {
          if (!token?.sub) return '';
          
          const sessionId = randomUUID();
          await adapter.createSession!({
              sessionToken: sessionId,
              userId: token.sub,
              expires: new Date( Date.now() + maxAge * 1000),
          })

          return sessionId
      },
  }

My more comprehensive solution is to have a flag that is set on the user returned by authorize(), then set it on the JWT object in callbacks.jwt, then finally use it switch off default encoding in jwt.encode. It seems to work fine and should guarantee not accidentally creating un-encoded JWTs when you should be encoding them. It does require some type extensions to make typescript happy, ie adding isCredentialsLogin to User and JWT. This solution avoids initialization overhead and hard-coding, but also seems to provide the same checks and safeguards as other solutions that use the req/res objects.

import { encode } from "@auth/core/jwt";
...
// authorize sets user.isCredentialsLogin = true on successful login
...
// session same as above
callbacks: {
        ...
        async jwt({token, user}) {
            token.isCredentialsLogin = user.isCredentialsLogin;
            return token;
        }
},
jwt: {
    async encode(params) {
        if (!params.token?.isCredentialsLogin) return encode(params);
        if (!params.token?.sub) return '';

        const sessionId = randomUUID();
        await adapter.createSession!({
            sessionToken: sessionId,
            userId: params.token.sub,
            expires: new Date( Date.now() + maxAge * 1000),
        })

        return sessionId;
    },
}

Are there any limitations to the above solution I'm missing? Only downside I can find is that you can't access the generateSessionToken method used in the package.

You must be logged in to vote
5 replies
@hillac
Comment options

Thanks for your answer. Is there a practical reason to create the session in the jwt callback rather than encode? Wouldn't encode always be called after jwt?

Also, can you elaborate on signIn creating the cookies? The cookies already get automatically created in my above configuration as far as I can tell.

@hillac
Comment options

Yeah I guess it looks nicer / more logical to make the token in jwt rather than encode and put the sessionId in the token like you did, and then still do the same checks in encode, but on token.sessionId, rather than isCredentialsLogin. But I think it would be functionally identical.

  callbacks: {
      ...
      async jwt({token, user}) {
          if (!user.isCredentialsLogin) return token;

          const session = await adapter.createSession!({
              sessionToken: randomUUID(),
              userId: user.id,
              expires: new Date( Date.now() + maxAge * 1000),
          });
          token.sessionId = session.sessionToken;
          return token;
      }
  },
  jwt: {
      async encode(params) {
          return params.token?.sessionId ??  encode(params);
      },
  }
@sobird
Comment options

use the account parameter and strategy flag will be simpler

  callbacks: {
      ...
      async jwt({token, user, account}) {
          if (account?.provider !== 'credentials' || sessionOptions.strategy === 'jwt') return token;

          const session = await adapter.createSession!({
              sessionToken: randomUUID(),
              userId: user.id,
              expires: new Date( Date.now() + maxAge * 1000),
          });
          token.sessionId = session.sessionToken;
          return token;
      }
  },
  jwt: {
      async encode(params) {
          return params.token?.sessionId ??  encode(params);
      },
  }
@hillac
Comment options

@sobird perfect!

@LeRoiLambda
Comment options

@hillac Thank you!
I keep getting an error and I dont know where it comes from, could you help me?

import NextAuth from "next-auth"
import authConfig from "@/auth.config"
import { generateId } from "@/lib/crypto"
import { authAdapter } from "@/lib/adapter"
import { PrismaAdapter } from "@auth/prisma-adapter"
import prisma from "@/lib/db"
import { encode } from "next-auth/jwt"

const SESSION_MAX_AGE = 30 * 24 * 60 * 60
const adapter = PrismaAdapter(prisma)
const sessionOptions = {
    strategy: "database",
    maxAge: SESSION_MAX_AGE,
    updateAge: 24 * 60 * 60,
}

export const {
    handlers: { GET, POST },
    auth,
    signIn,
} = NextAuth({
    adapter: adapter,
    pages: {
        signIn: "/login",
        signOut: "/signout",
        newUser: "/verify-email",
    },
    session: sessionOptions,
    callbacks: {
        async jwt({ token, user, account }) {
            if (account?.provider !== "credentials" || sessionOptions.strategy === "jwt")
                return token
            console.log("user: ", user)
            console.log("account: ", account)
            const session = await adapter.createSession({
                expiresAt: new Date(Date.now() + SESSION_MAX_AGE * 1000),
                token: generateId(36),
                userId: user.id,
            })
            
            console.log("session: ", session)
            token.id = session.token
            console.log("token1: ", token)
            return token
        },
        async session({ session: defaultSession, user }) {
            console.log("defaultSession: ", defaultSession)
            console.log("user: ", user)
            // Make our own custom session object.

            return session
        },
    },
    jwt: {
        async encode(params) {
            console.log("token2: ", params.token)
            return params.token.id ?? encode(params)
        },
    },
    ...authConfig,
})
[auth][error] JWTSessionError: Read more at https://errors.authjs.dev#jwtsessionerror
[auth][cause]: JWEInvalid: Invalid Compact JWE
    at compactDecrypt (webpack-internal:///(middleware)/./node_modules/jose/dist/browser/jwe/compact/decrypt.js:20:15)
    at jwtDecrypt (webpack-internal:///(middleware)/./node_modules/jose/dist/browser/jwt/decrypt.js:12:100)
    at Object.decode (webpack-internal:///(middleware)/./node_modules/@auth/core/jwt.js:78:79)
    at Module.session (webpack-internal:///(middleware)/./node_modules/@auth/core/lib/actions/session.js:23:39)
    at AuthInternal (webpack-internal:///(middleware)/./node_modules/@auth/core/lib/index.js:47:77)
    at async Auth (webpack-internal:///(middleware)/./node_modules/@auth/core/index.js:126:34)

The error come from the return of async encode

Here are the output of the consoles logs:

User in the auth config file:  {
  id: '232ef022-d886-43ec-86f4-886bf020a6e9',
  firstName: 'Val',
  lastName: 'V',
  email: 'myemail@protonmail.ch',
  companyName: 'Company',
  country: 'US',
  password: '$2b$12$dnNCPmNI9EeOhWPwyi2AOuU8dtLh6strVmy2jwXtHCm9soqChSmua',
  createdAt: 2024-03-28T11:06:00.917Z,
  updatedAt: 2024-03-28T11:06:00.917Z,
  verifiedAt: null
}
user:  {
  id: '232ef022-d886-43ec-86f4-886bf020a6e9',
  firstName: 'Val',
  lastName: 'Mei',
  email: 'myemail@protonmail.ch',
  companyName: 'Charge',
  country: 'US',
  password: '$2b$12$dnNCGmNI9EeOhWswyi2AOuU8dtLh6strEmg2jwXtHCe9soqChSmua',
  createdAt: 2024-03-28T11:06:00.917Z,
  updatedAt: 2024-03-28T11:06:00.917Z,
  verifiedAt: null
}
account:  {
  providerAccountId: '232ef022-d886-43ec-86f4-886bf020a6e9',
  type: 'credentials',
  provider: 'credentials'
}
session:  {
  id: 5,
  token: '7q0R5cAJX7I2bROdcgPLVKIiNdfFHiihtQoV',
  expiresAt: 2024-04-28T04:43:15.398Z,
  userId: '232ef022-d886-43ec-86f4-886bf020a6e9'
}
token1:  {
  name: undefined,
  email: 'myemail@protonmail.ch',
  picture: undefined,
  sub: '232ef022-d886-43ec-86f4-886bf020a6e9',
  id: '7q0R5cAJX7I2bROdcgPLVKIiNdfFHiihtQoV'
}
token2:  {
  name: undefined,
  email: 'myemail@protonmail.ch',
  picture: undefined,
  sub: '232ef022-d886-43ec-86f4-886bf020a6e9',
  id: '7q0R5cAJX7I2bROdcgPLVKIiNdfFHiihtQoV'
}

If you could help me it would be amazing,

Many thanks

Comment options

You must be logged in to vote
2 replies
@mp3por
Comment options

Hello, I have been thinking for a long time to do that, but always so busy with work. I would love to make a fork of this library and just "fix" everything around the Credentials provider, so that it could be used as normal.

@seh-GAH-toh
Comment options

This, this is fucking god work right there, I've spent the last 3 days trying to get the credential provider to work. At this point, I'm just going to write my own authenticator that will be more efficient. Huge thanks @Ahmadh26

Comment options

Does someone have a working example and a repo using Next.JS 14 app router? I have only managed it to work with pages directory unfortunately...

Thanks in advance

You must be logged in to vote
1 reply
@nneko
Comment options

@GnussonNet you can get this to run in Next 13/14 using the app router by doing the following modifications:

  1. Create the api route in a file at the path below. Note, while NEXT.js does support creating routes outside the /app/api folder next-auth requires it to be in this location

app/api/auth/[...nextauth]/route.js

  1. Do advanced initialization
//Import the necessary database adapters. In this case we are using Prisma but chose the one appropriate for your use case
import { PrismaAdapter } from "@auth/prisma-adapter"

...

//db is the database client object for your chosen database
const adapter = PrismaAdapter(db)
...

const authHandler = async (req, res) => {
	const callbacks = {

...



	// Trigger the next-auth authentication flow at the end of the advanced initiatilization
	return await NextAuth(req, res, options)
}

export { authHandler as GET, authHandler as POST }

  1. Import the updated NEXT.js cookies module

import { cookies } from "next/headers"
import { encode, decode } from "next-auth/jwt"

  1. Adjust the signIn callback in the authHandler with code similar to the following:
async signIn({ user, account, profile, email, credentials }) {

...

if (
				req.url.includes("callback") &&
				req.url.includes("credentials") &&
				req.method === "POST"
			) {

...

                                                const sessionToken = generateSessionToken()
						const sessionExpiry = fromDate(session.maxAge)

						const createdSession = await adapter?.createSession({
							sessionToken: sessionToken,
							userId: user.id,
							expires: sessionExpiry,
						})

						if (!createdSession) return false

						const cks = cookies()
						cks.set({
							name: "next-auth.session-token",
							value: sessionToken,
							expires: sessionExpiry,
						})

...

return true;
}

  1. Ensure you use the follow JWT options to override the jwt token when using the credentials adapter
		jwt: {
			maxAge: 60 * 60 * 24 * 30,
			// Customize the JWT encode and decode functions to overwrite the default behaviour of storing the JWT token in the session cookie when using credentials providers. Instead we will store the session token reference to the session in the database.
			encode: async (token, secret, maxAge) => {
				if (
					req.url.includes("callback") &&
					req.url.includes("credentials") &&
					req.method === "POST"
				) {
					const cks = cookies()

					const cookie = cks?.get("next-auth.session-token")

					if (cookie) return cookie.value
					else return ""
				}
				// Revert to default behaviour when not in the credentials provider callback flow
				return encode(token, secret, maxAge)
			},
			decode: async (token, secret) => {
				if (
					req.url.includes("callback") &&
					req.url.includes("credentials") &&
					req.method === "POST"
				) {
					return null
				}

				// Revert to default behaviour when not in the credentials provider callback flow
				return decode(token, secret)
			},
		},
Comment options

Latest update if you want to persist the session token in database for CredentialsProvider.
Environment: Auth.js beta 18 and Next.js App router.

Rationale: Next Auth by default uses JWT strategy for CredentialsProvider even if you set session strategy to database. So what we want to do here is simply 'deceiving' the JWT token encoding when CredentialsProvider is used to return the session id instead of a JWT token string

I use Upstash Redis for my database but it should be very straightforward for other databases too.

import type { NextAuthConfig, Session } from "next-auth";
import GoogleProvider from "next-auth/providers/google";
import TwitterProvider from "next-auth/providers/twitter";
import { signInUrl } from "@/lib/info/constants";
import { UpstashRedisAdapter } from "@auth/upstash-redis-adapter";
import { redis, redisKeyOptions, sessionExpiry } from "../upstash/redis";
import { PostmarkEmailProvider } from "../postmark/EmailProvider";
import { credentialsProvider } from "./credentials";
import { v4 as uuid } from "uuid";
import { encode as defaultEncode } from "next-auth/jwt";

export const authConfig: NextAuthConfig = {
  providers: [
    credentialsProvider,
    GoogleProvider({
      id: "google",
      clientId: process.env.AUTH_GOOGLE_ID!,
      clientSecret: process.env.AUTH_GOOGLE_SECRET!,
      allowDangerousEmailAccountLinking: true,
    }),
    TwitterProvider({
      id: "x",
      clientId: process.env.AUTH_X_ID!,
      clientSecret: process.env.AUTH_X_SECRET!,
    }),
    PostmarkEmailProvider,
  ],
  callbacks: {
    async jwt({ token, user, account }) {
      if (account?.provider === "credentials") {
        token.credentials = true;
      }
      return token;
    },
  },
  jwt: {
    encode: async function (params) {
      if (params.token?.credentials) {
        const sessionToken = uuid();
        await redis.set(
          `${redisKeyOptions.baseKeyPrefix}${redisKeyOptions.sessionKeyPrefix}${sessionToken}`,
          JSON.stringify({
            sessionToken,
            userId: params.token.sub,
            expires: new Date(Date.now() + sessionExpiry * 1000),
          }),
          {
            ex: sessionExpiry,
          },
        );
        await redis.set(
          `${redisKeyOptions.baseKeyPrefix}${redisKeyOptions.sessionByUserIdKeyPrefix}${params.token.sub}`,
          sessionToken,
          {
            ex: sessionExpiry,
          },
        );
        return sessionToken;
      }
      return defaultEncode(params);
    },
  },
  pages: {
    signIn: signInUrl,
    verifyRequest: "/auth/verify-request",
    error: "/auth/retry",
    signOut: "auth/signout",
  },
  adapter: UpstashRedisAdapter(redis, redisKeyOptions),
  secret: process.env.AUTH_SECRET!,
};
You must be logged in to vote
2 replies
@songhobby
Comment options

My configuration for the CredentialsProvider if interested.

import Credentials from "next-auth/providers/credentials";
import { redis, redisKeyOptions } from "../../upstash/redis";
import { CredentialsSignin, User } from "next-auth";
import { generateSalt, hashPassword } from "./password";
import { emailSchema, passwordSchema } from "./schema";

class UserNotFound extends CredentialsSignin {
  code = "UserNotFound";
}
class InvalidCredentials extends CredentialsSignin {
  code = "InvalidCredentials";
}
class PasswordNotSet extends CredentialsSignin {
  code = "PasswordNotSet";
}

export const credentialsProviderId = "credentials:";

export const credentialsProvider = Credentials({
  credentials: {
    email: {},
    password: {},
  },
  async authorize(credentials) {
    const { email, password } = credentials;
    await Promise.all([
      emailSchema.validate(email),
      passwordSchema.validate(password),
    ]);

    const userId = await redis.get<string | null>(
      `${redisKeyOptions.baseKeyPrefix}${redisKeyOptions.emailKeyPrefix}${email}`,
    );
    if (!userId) {
      throw new UserNotFound();
    }

    const passwordDigest = await redis.get<string | null>(
      `${redisKeyOptions.baseKeyPrefix}${redisKeyOptions.accountKeyPrefix}${credentialsProviderId}${userId}`,
    );
    if (!passwordDigest) {
      throw new PasswordNotSet();
    }
    const [salt, hash] = passwordDigest.split(":");
    const passwordHash = hashPassword(salt, password as string);

    if (passwordHash !== hash) {
      throw new InvalidCredentials();
    }

    const newSalt = generateSalt();
    const newPasswordHash = hashPassword(newSalt, password as string);
    await redis.set(
      `${redisKeyOptions.baseKeyPrefix}${redisKeyOptions.accountKeyPrefix}${credentialsProviderId}${userId}`,
      `${newSalt}:${newPasswordHash}`,
    );
    const user = await redis.get<User>(
      `${redisKeyOptions.baseKeyPrefix}${redisKeyOptions.userKeyPrefix}${userId}`,
    );
    return user;
  },
});
@NickBolles
Comment options

Amazing. Thank you! This is all I had to add with auth.js to fix the issue. One key change from yours is re-using the adapter with authConfig.adapter.createSession

	callbacks:{
	    async jwt({ token, account }) {
	      if (account?.provider === 'wix-jwt') {
	        token['credentials'] = true;
	      }
	      return token;
	    },   
    }
    jwt: {
	    encode: async function (params) {
		  // If the callbacks.jwt callback added the credentials flag, handle encoding by manually creating the session
	      if (params.token?.['credentials']) {
	        const sessionToken = randomUUID(); 

			// Use the adapter to create a new session
	        await authConfig.adapter?.createSession({
	          sessionToken,
	          userId: params.token.sub,
	          expires: new Date(Date.now() + sessionMaxAge * 1000),
	        });
	
			// return the session ID instead of the default encoded JWT
	        return sessionToken;
	      }
	      return defaultEncode(params);
	    },
	  },
Comment options

Thanks for @nneko the solutions is worked, but i'm having hard time to figure out how to implement those handler
For anyone that confuse how to implement custom handler on routes here is the snippet for src/app/api/auth/[...nextauth]/route.ts

I'm using t3 templates from https://github.com/t3-oss/create-t3-app

import { type NextApiRequest, type NextApiResponse } from "next";
import NextAuth, { type NextAuthOptions } from "next-auth";
import { decode, encode } from "next-auth/jwt";
import { v4 as uuidV4 } from "uuid";

import { authOptions } from "~/server/auth";

const handler = async (req: NextApiRequest, res: NextApiResponse) => {
  const callbacks: NextAuthOptions["callbacks"] = {
    ...authOptions.callbacks,
    async signIn({ user, account, profile, email, credentials }) {
      if (
        req.url?.includes("callback") &&
        req.url.includes("credentials") &&
        req.method === "POST"
      ) {
        const sessionToken = generateSessionToken();
        const sessionExpiry = fromDate(
          authOptions.session?.maxAge ?? 30 * 24 * 60 * 60,
        );

        const createdSession = await dbAdapter?.createSession?.({
          sessionToken: sessionToken,
          userId: user.id,
          expires: sessionExpiry,
        });

        if (!createdSession) return false;

        const cks = cookies();
        cks.set({
          name: "next-auth.session-token",
          value: sessionToken,
          expires: sessionExpiry,
        });
      }

      return true;
    },
  };

  const jwt: NextAuthOptions["jwt"] = {
    ...authOptions.jwt,
    maxAge: 60 * 60 * 24 * 30,
    // Customize the JWT encode and decode functions to overwrite the default behaviour of storing the JWT token in the session cookie when using credentials providers. Instead we will store the session token reference to the session in the database.
    encode: async ({ token, secret, maxAge }) => {
      if (
        req.url?.includes("callback") &&
        req.url.includes("credentials") &&
        req.method === "POST"
      ) {
        const cks = cookies();

        const cookie = cks?.get("next-auth.session-token");

        if (cookie) return cookie.value;
        else return "";
      }
      // Revert to default behaviour when not in the credentials provider callback flow
      return encode({
        token,
        secret,
        maxAge,
      });
    },
    decode: async ({ token, secret }) => {
      if (
        req.url?.includes("callback") &&
        req.url.includes("credentials") &&
        req.method === "POST"
      ) {
        return null;
      }

      // Revert to default behaviour when not in the credentials provider callback flow
      return decode({ token, secret });
    },
  };

  // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-argument
  return await NextAuth(req, res, {
    ...authOptions,
    callbacks,
    jwt,
  });
};

export { handler as GET, handler as POST };

const generate = {
  uuid: () => {
    return uuidV4();
  },
};

// Modules needed to support key generation, token encryption, and HTTP cookie manipulation
import { randomUUID } from "crypto";
import { cookies } from "next/headers";
import { dbAdapter } from "~/server/auth/db-adapter";

// Helper functions to generate unique keys and calculate the expiry dates for session cookies
const generateSessionToken = () => {
  // Use `randomUUID` if available. (Node 15.6++)
  return randomUUID?.() ?? generate.uuid();
};

const fromDate = (time: number, date = Date.now()) => {
  return new Date(date + time * 1000);
};
You must be logged in to vote
2 replies
@ProSystemLimited
Comment options

I want to be able to revoke a user's session by deleting it from the database. In this code, I can see that the session is being stored in the database upon sign in, but in the jwt callback, the session token is being retrieved from the cookie. Can someone please explain which part of the code checks the session from the database?

@taylor-lindores-reeves
Comment options

I want to be able to revoke a user's session by deleting it from the database. In this code, I can see that the session is being stored in the database upon sign in, but in the jwt callback, the session token is being retrieved from the cookie. Can someone please explain which part of the code checks the session from the database?

Pretty sure it's the JWT encode() event. You can return token.id and it will use the token as the reference point for the database session.

Comment options

I faced the same issue where using CredentialsProvider in NextAuth.js didn't automatically call the custom Sequelize adapter's createSession, unlike OAuth providers.

I resolved it by implementing a custom jwt.encode callback that manually creates the session in the DB and returns a custom sessionToken. This way, session syncing works correctly with getSession and getServerSession:

jwt: {
      async encode(params) {
           try {
                 const sessionToken = uuidv4();

                 await SequelizeModelService.create(SessionModel, {
                        session_token: sessionToken,
                        user_id: params.token?.sub!,
                        expires: new Date(Date.now() + NEXT_AUTH_DB_SESSION_EXPIRY),
                  });

                 return sessionToken;
            }
            catch (error) {
                 logger.error(`${prefix} session token creation failed`, error);
            }

            return '';
     }
  }
You must be logged in to vote
1 reply
@maiconcarraro
Comment options

this approach also works in v5 beta 💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Help
Labels
None yet
Morty Proxy This is a proxified and sanitized view of the page, visit original site.