-
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. 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? |
Beta Was this translation helpful? Give feedback.
Replies: 35 comments 107 replies
-
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 |
Beta Was this translation helpful? Give feedback.
-
Link to the docs: https://next-auth.js.org/providers/credentials |
Beta Was this translation helpful? Give feedback.
-
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
api/auth/signup.js
pages/register.js
pages/login.js
|
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
Has anyone been able to deploy this on Vercel, or any other provider? |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
If, like me, your motivation for wanting this is to be able to revoke user's sessions on logout, I found a solution.
|
Beta Was this translation helpful? Give feedback.
-
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
// 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 |
Beta Was this translation helpful? Give feedback.
-
I was able to use database strategy with credentials provider and everything else works perfectly. here is the comment he provided: and his blogpost about the issue: |
Beta Was this translation helpful? Give feedback.
-
For someone who needs a complete code using next-auth Credentials + session + DB adapter, here's the final [...nextauth].js file. 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))
}
|
Beta Was this translation helpful? Give feedback.
-
So I have to handle the session creation myself if I use credentials to log in? |
Beta Was this translation helpful? Give feedback.
-
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
Type '(req: NextApiRequest, res: NextApiResponse) => Promise' has no properties in common with type 'GetServerSessionOptions'. |
Beta Was this translation helpful? Give feedback.
-
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)
|
Beta Was this translation helpful? Give feedback.
-
Thank you all for the workarounds. I am curious why not make this a builtin function that is closed by default, nowadays it is still hard to not support username/password completely. |
Beta Was this translation helpful? Give feedback.
-
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? |
Beta Was this translation helpful? Give feedback.
-
Hi all, I come up with another way to solve the problem. Instead of persist credential info in database, I give different 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));
} |
Beta Was this translation helpful? Give feedback.
-
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:
The The A 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. |
Beta Was this translation helpful? Give feedback.
-
Can Anyone provide a suitable SignUp code for this approach? |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
My only worry remains how these workarounds would integrate with V5 when it's out. |
Beta Was this translation helpful? Give feedback.
-
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
src/app/api/auth/[...nextauth]/route.ts
|
Beta Was this translation helpful? Give feedback.
-
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 2023app/api/auth/[...nextauth]/route.ts
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.tsxstatus.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"] }; |
Beta Was this translation helpful? Give feedback.
-
The solution for me to add |
Beta Was this translation helpful? Give feedback.
-
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. |
Beta Was this translation helpful? Give feedback.
-
What's up guys, so after messing around with some of the solutions in here and settling on one, I actually ended up upgrading to Auth v5 and writing my own solution after actually taking a look through the source code of the library itself. I'm just going to present my code mostly as is since the concept should be transferable to your projects. Note that this won't work with just the auth.ts import 'server-only';
import NextAuth from 'next-auth';
import type { Session } from 'next-auth/types';
import Credentials from 'next-auth/providers/credentials';
import GoogleProvider from 'next-auth/providers/google';
import {
SESSION_MAX_AGE,
} from '@path/to/config';
import { authAdapter } from '@path/to/adapter';
import { generateId } from '@server/helpers/generateId';
export const providers = [
Credentials({
credentials: {
username: {
label: 'Username or Email',
type: 'username',
},
password: {
label: 'Password',
type: 'password',
},
},
async authorize(credentials) {
try {
// Do password stuff here and return the user.
} catch (error) { ... }
},
}),
GoogleProvider({
clientId: <id>,
clientSecret: <secret>,
authorization: {
params: {
scope: 'email',
},
},
httpOptions: {
timeout: 10000,
},
async profile(profile) {
...
return profile;
},
}),
];
export const {
handlers: { GET, POST },
auth,
signIn,
} = NextAuth({
adapter: authAdapter(),
callbacks: {
async jwt({ user }) {
// Override default jwt callback behavior.
// Create a session instead and then return that session token for use in the
// `jwt.encode` callback below.
const session = await authAdapter().createSession?.({
expires: new Date(Date.now() + SESSION_MAX_AGE * 1000),
sessionToken: generateId().token(),
userId: user.id,
});
return { id: session?.sessionToken };
},
async session({ session: defaultSession, user }) {
// Make our own custom session object.
const session: Session = {
user: { ... },
expires: defaultSession.expires,
};
return session;
},
},
jwt: {
async encode({ token }) {
// This is the string returned from the `jwt` callback above.
// It represents the session token that will be set in the browser.
return token?.id as unknown as string;
},
async decode() {
// Disable default JWT decoding.
// This method is really only used when using the email provider.
return null;
},
},
providers,
session: {
strategy: 'database',
maxAge: SESSION_MAX_AGE,
generateSessionToken: () => generateId().token(),
},
}); I also coded my own Adapter as I wasn't a fan of how much extra data was taken up by NextAuth, especially since I'm not doing anything with the OAuth information after the fact. adapter.ts import 'server-only';
import type { Adapter, AdapterUser } from '@auth/core/adapters';
import { Awaitable } from '@auth/core/types';
import { prisma } from '@path/to/prisma';
import { generateId } from "@server/helpers/generateId";
import { getIP } from 'utils';
import { getUserAgent } from 'utils';
type AuthAdapter = {
// Create type for createSession
createUser: (
// eslint-disable-next-line no-unused-vars
data: Pick<AdapterUser, 'username' | 'email' | 'displayName'>,
) => Awaitable<AdapterUser>;
} & Omit<Adapter, 'createUser'>;
export function authAdapter(): AuthAdapter {
return {
async createSession(data) {
// Collect IP and UserAgent information here.
const createSessionResponse = await prisma.session.create({
data: {
token: data.sessionToken,
userId: data.userId,
expiresAt: data.expires,
ip: getIP(),
useragent: getUserAgent(),
},
});
return {
expires: createSessionResponse.expiresAt,
sessionToken: createSessionResponse.token,
userId: createSessionResponse.userId,
};
},
async createUser(data) {
const userData = {
id: generateId(),
username: data.username,
email: data.email,
displayName: data.displayName,
createdAt: new Date(),
};
const newUser = await prisma.user.create({
data: userData,
});
return newUser;
},
async deleteSession(token) {
const session = await prisma.session.delete({ where: { token } });
return {
expires: session.expiresAt,
sessionToken: session.token,
userId: session.userId,
};
},
async getSessionAndUser(token) {
const userAndSession = await prisma.session.findUnique({
where: { token },
include: { user: true },
});
if (!userAndSession) return null;
const { user, ...session } = userAndSession;
return {
user,
session: {
expires: session.expiresAt,
sessionToken: session.token,
userId: session.userId,
},
};
},
async getUserByAccount(oAuthProvider) {
const { provider, providerAccountId } = oAuthProvider;
// We'll only ever use Google or Apple, so we can just check for those.
if (provider === 'google') {
return prisma.user.findFirst({
where: { googleId: providerAccountId },
});
}
return null;
},
async updateSession(data) {
const session = await prisma.session.update({
where: { token: data.sessionToken },
data: { token: data.sessionToken, expiresAt: data.expires },
});
return {
expires: session.expiresAt,
sessionToken: session.token,
userId: session.userId,
};
},
deleteUser: (id) => prisma.user.delete({ where: { id } }),
getUser: (id) => prisma.user.findUnique({ where: { id } }),
getUserByEmail: (email) => prisma.user.findUnique({ where: { email } }),
linkAccount: async (data) => {
if (data.provider === 'google') {
const user = await prisma.user.update({
where: { id: data.userId },
data: {
googleId: data.providerAccountId,
},
});
if (user?.googleId)
return {
provider: data.provider,
providerAccountId: user.googleId,
type: data.type,
userId: user.id,
};
}
return null;
},
// unlinkAccount: (oAuthProvider) => {
// Just do the opposite of linkAccount.
// },
updateUser: ({ id, ...data }) => prisma.user.update({ where: { id }, data }),
};
} Now my schema is able to be a lot more efficient like this: prisma.schema
No need for a separate accounts table. I also had to update the types somewhat. I'm not great at type modifications so it might not be written the best: auth.d.ts
Overall not too bad once I took a look at the source code to see what Auth was doing behind the scenes. Honestly though it might be easier to just fork your own copy of Auth and make the changes yourself. Within the package itself only like 10 lines of code would need to be deleted or modified to get credentials working without any catches (literally mostly deleting boolean checks). I wish the team wouldn't make it so difficult to do it, but after working through this and also implementing my own credentials provider I fully get their concerns about security. But they should leave that up to the user, not themselves imo. |
Beta Was this translation helpful? Give feedback.
-
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 |
Beta Was this translation helpful? Give feedback.
-
Latest update if you want to persist the session token in database for CredentialsProvider. Rationale: Next Auth by default uses JWT strategy for CredentialsProvider even if you set session strategy to I use Upstash Redis for my database but it should be very straightforward for other databases too.
|
Beta Was this translation helpful? Give feedback.
-
Thanks for @nneko the solutions is worked, but i'm having hard time to figure out how to implement those handler 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);
}; |
Beta Was this translation helpful? Give feedback.
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/