I've setup my Nextjs (Next12) with NextAuth CredentialsProvider and use Prisma Adapter to persist user's session in the database.
I followed this documentation here from NextAuth team themselves. But nothing happen after I clicked on login button.
To Note
Before that:-
I've make sure to try get the data first from the database & it works just fine.
I did also did try to just use the normal session: { jwt: true, maxAge: 30 * 24 * 60 * 60 } instead of straight away use Adapter. Also works fine.
Question
Now, I just want to know whether it's possible or not to use CredentialsProvider with Adapter at all?
NextAuth API
Below are 2 examples or working one and not working one: /pages/api/auth/[...nextauth].js
working: does not use adapter
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
export default async function auth(req, res) {
return await NextAuth(req, res, {
secret: process.env.SECRET,
adapter: PrismaAdapter(prisma),
session: {
jwt: true,
maxAge: 30 * 24 * 60 * 60, // 30 days
}
providers: [
CredentialsProvider({
async authorize(credentials) {
const user = await prisma.user.findFirst({
where: {
email: credentials.email,
password: credentials.password
}
});
if (user !== null)
{
return user;
}
else {
throw new Error('User does not exists. Please make sure you insert the correct email & password.')
}
}
})
],
callbacks: {
redirect: async ({ url, baseUrl }) => {
return baseUrl
},
jwt: async ({ token, user, account, profile, isNewUser }) => {
if (typeof user !== typeof undefined) token.user = user;
return token
},
session: async ({ session, user, token }) => {
token?.user && (session.user = token.user)
return session
}
}
})
}
not working: using prisma adapter
import { PrismaAdapter } from "#next-auth/prisma-adapter";
import { PrismaClient } from '#prisma/client';
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const prisma = new PrismaClient()
export default async function auth(req, res) {
return await NextAuth(req, res, {
secret: process.env.SECRET,
adapter: PrismaAdapter(prisma),
providers: [
CredentialsProvider({
async authorize(credentials) {
const user = await prisma.user.findFirst({
where: {
email: credentials.email,
password: credentials.password
}
});
if (user !== null)
{
return user;
}
else {
throw new Error('User does not exists. Please make sure you insert the correct email & password.')
}
}
})
],
callbacks: {
redirect: async ({ url, baseUrl }) => {
return baseUrl
},
jwt: async ({ token, user, account, profile, isNewUser }) => {
if (typeof user !== typeof undefined) token.user = user;
return token
},
session: async ({ session, user, token }) => {
token?.user && (session.user = token.user)
return session
}
}
})
}
Prisma Schema
This is the current schema.prisma (this comes from the NextAuth doc itself):-
I already did the npx prisma migrate dev & npx prisma generate
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
// shadowDatabaseUrl = env("SHADOW_URL")
referentialIntegrity = "prisma"
}
model Account {
id String #id #default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
oauth_token_secret String?
oauth_token String?
user User #relation(fields: [userId], references: [id], onDelete: Cascade)
##unique([provider, providerAccountId])
}
model Session {
id String #id #default(cuid())
sessionToken String #unique
userId String
expires DateTime
user User #relation(fields: [userId], references: [id], onDelete: Cascade)
}
model User {
id String #id #default(cuid())
name String?
email String? #unique
password String?
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model VerificationToken {
identifier String
token String #unique
expires DateTime
##unique([identifier, token])
}
I just found out the CredentialsProvider from next-auth will not work with adapter at all according to this doc. You may find the reason why it can't or not recommended to use with CredentialsProvider.
This is an additional link where you can find a discussion regarding this matter.
Related
This is the error
"message": "Unable to find Prisma Client in GraphQL context. Please provide it under the context["customPrisma"] key"
stacktrace
stacktrace": [
"Error: Unable to find Prisma Client in GraphQL context. Please provide it under the `context[\"customPrisma\"]` key.",
" at getPrismaFromContext (webpack-internal:///(api)/./prisma/generated/type-graphql/helpers.js:25:15)",
" at FacturaCrudResolver.createOneFactura (webpack-internal:///(api)/./prisma/generated/type-graphql/resolvers/crud/Factura/FacturaCrudResolver.js:42:51)",
" at C:\\Users\\felip\\OneDrive\\Documentos\\evergreen-fin\\evergreen-facturacion\\node_modules\\type-graphql\\dist\\resolvers\\create.js:31:98",
" at process.processTicksAndRejections (node:internal/process/task_queues:95:5)"
]
}
}
}
I'm working with Graphql and Apollo server to test a mutation that i need to send some data to the DB. the GQL server API in nextJS is the next one:
import 'reflect-metadata';
import 'ts-tiny-invariant'
import { NextApiRequest, NextApiResponse } from 'next';
import { PrismaClient } from '#prisma/client';
import { ApolloServer } from 'apollo-server-micro';
import Cors from 'micro-cors'
import { resolvers } from '../../prisma/generated/type-graphql';
import { buildSchema } from 'type-graphql';
//import prisma from '../../config/prisma'
const prisma = new PrismaClient();
const cors = Cors({
allowMethods: ['POST', 'GET', 'OPTIONS', 'HEAD']
})
interface Context {
p: PrismaClient;
}
export const config = {
api:{
bodyParser: false,
},
}
const serverGraphql = async (req: NextApiRequest, res: NextApiResponse) => {
const schema = await buildSchema({
resolvers: resolvers,
validate: false
})
const server = new ApolloServer({
schema: schema,
introspection: true,
context: (): Context => ({ p: prisma })
});
const start = server.start()
await start;
return server.createHandler({
path: '/api/graphql'
})(req, res)
}
export default cors((req:any, res:any) => {
if (req.method === "OPTIONS") {
res.end()
return false;
}
return serverGraphql(req,res);
})
In the documentation of Type-graphql i see tha i need to add the context to the generator of the .prisma file where i put the models for migrate the DB
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
generator typegraphql {
provider = "typegraphql-prisma"
output = "../prisma/generated/type-graphql"
contextPrismaKey = "customPrisma" -> THERE IS THE CUSTOMPRISMA WHERE I SEE THE ERROR IN Apollo server
emitTranspiledCode = true
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int #id #default(autoincrement())
email String #unique
nombre String?
role Role #default(CLIENT)
facturas Factura[]
profile Profile?
}
model Profile {
id Int #id #default(autoincrement())
bio String
user User #relation(fields: [userId], references: [id])
userId Int #unique
}
model Factura {
id Int #id #default(autoincrement())
createdAt DateTime #default(now())
moneda String #db.VarChar(3)
monto Float
pais String
esEfectivo String? #default("efectivo")
published Boolean #default(true)
cliente User #relation(fields: [clienteId], references: [id])
clienteId Int
}
enum Role {
CLIENT
ADMIN
}
The Mutation is the next one
In the prisma schema, change contextPrismaKey to p instead of customPrisma
I am trying to use blitz.js login API in a Flutter project. So I have created a /api/auth/login.ts file with the following code
import { getAntiCSRFToken, getSession, SecurePassword } from "#blitzjs/auth"
import { authenticateUser } from "app/auth/mutations/login"
import { AuthenticationError } from "blitz"
import db from "db"
import { Role } from "types"
const handler = async (req, res) => {
const session = await getSession(req, res)
const { email, password } = req.body
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` })
} else {
console.log("Create a new session for the user")
// Create a new session for the user
//login
const user = await authenticateUser(email, password)
const user = await db.user.findFirst({ where: { email } })
if (!user) return res.json({ data: "Hello", email, password })
const result = await SecurePassword.verify(user.hashedPassword, password)
const { hashedPassword, ...rest } = user
await req.session.$create({ userId: user.id, role: user.role as Role })
res.json({ rest })
}
export default handler
I also tried to use their docs but it was not clear enough and understandable
Can I use ctx.session.$create and insert it to db using blitz.js api
I have solved the problem using this code
import { Role } from "types"
import { authenticateUser } from "app/auth/mutations/login"
import { getSession } from "#blitzjs/auth"
export default async function customRoute(req, res) {
const session = await getSession(req, res)
const { email, password } = req.body
console.log(email, password)
console.log(session.$isAuthorized())
const user = await authenticateUser(email, password)
if (user.id === session.userId) {
return res.status(409).json({ error: `Already exist` })
}
await session.$create({ userId: user.id, role: user.role as Role })
// // res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({ userId: session.userId }))
}
At first, I was getting a CSRF mismatch error and then a localStorage is undefined and now somehow everything is working with this code.
This is code in api/auth/[...nextAuth].js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "#/models/user";
import connectToDb from "#/config/dbConnection";
export default NextAuth({
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
async authorize(credentials) {
connectToDb();
const { email, password } = credentials;
// check if email and passwird entered
if (!email || !password) {
throw new Error("please enter email or password");
}
const user = await User.findOne({
email,
// becasue in pre.save I excluded returning password in api response
}).select("+password");
if (!user) {
throw new Error("Invalid email or password");
}
// check if password is correct
// I added comparePassword to userSchema methods in mongoose
const isPasswordMatched = await user.comparePassword(password);
if (!isPasswordMatched) {
throw new Error("please enter email or password");
}
// creating a promise
return Promise.resolve(user);
},
}),
],
callbacks: {
// jwt callback is only called when token is created
jwt: async ({ token, user }) => {
// user is obj that we have received from authorize Promise.resolve(user)
user && (token.user = user);
// not this token has user property
return Promise.resolve(token);
},
// user arg here is actully token that returned from jwt.
session: async ({ session, token }) => {
// session callback is called whenever a session for that particular user is checked
console.log("user in ...next auth api", token);
session.user = token.user;
// since I get error, I return Promise.resolve(session)
return Promise.resolve(session);
},
},
});
then I write a middleware to get the session:
const isAuthenticatedUser = asyncErrors(async (req, res, next) => {
// when user logged in we create the session of user
// getSession gets auth-token from req.headers
const session = await getSession({ req });
// THIS RETURNS NULL
console.log("session", session);
if (!session) {
return next(new ErrorHandler("Login first to access this resource", 401));
}
req.user = session.user;
next();
});
export { isAuthenticatedUser };
I write api/me to check the user
import nc from "next-connect";
import connectToDb from "#/config/dbConnection";
import { currentUserProfile } from "#/controllers/authController";
import { isAuthenticatedUser } from "#/middlewares/auth";
import onError from "#/middlewares/errors";
const handler = nc({ onError });
connectToDb();
handler.use(isAuthenticatedUser).get(currentUserProfile);
export default handler;
When I visit http://localhost:3000/api/me, since session is logged as null, I get not authneticated, even though, in chrome debugging tools' Aplication tab next-auth credentials are present
Since https://next-auth.js.org/getting-started/upgrade-v4
Version 4 makes using the SessionProvider mandatory.
this is _app.js component
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
// `session` comes from `getServerSideProps` or `getInitialProps`.
// Avoids flickering/session loading on first load.
<SessionProvider session={session} refetchInterval={5 * 60}>
<Component {...pageProps} />
</SessionProvider>
)
}
This problem is happening due to not specifying e.preventDefault() on login button.
The working code should look like this :-
async function login(e) {
e.preventDefault(); //Add this to your code.
const getLoginStatus = await signIn("credentials", {
redirect: false,
username,
password,
})};
just wanna have my custom credential provider which authenticate the entered username and password with Firebase Authentication on sign in page
pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
import { getDatabase } from "firebase/database"
import { DB } from "../../../constants/firebase"
import { FirebaseAdapter } from "#next-auth/firebase-adapter"
import * as firestoreFunctions from "firebase/firestore"
import CredentialsProvider from "next-auth/providers/credentials"
export default NextAuth({
session: {
strategy: "database",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "somebody#gmail.com",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const database = getDatabase()
console.log(database)
const user = {
id: 1,
usename: "j",
password: "123456789",
}
if (
credentials?.username === user.usename &&
credentials.password === "123456789"
) {
return user
}
return null
},
}),
],
adapter: FirebaseAdapter({
db: DB,
...firestoreFunctions,
}),
// pages: {
// signIn: "/auth/signin",
// signOut: "/auth/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)
// },
callbacks: {
async jwt({ token, user }) {
if (user) {
token.email = user.email
}
return token
},
async session({ session, token, user }) {
if (token) {
session.user!.email = token.email
}
return session
},
redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) return url
else if (url.startsWith("/"))
return new URL(url, baseUrl).toString()
return baseUrl
},
},
})
firebase.ts
import { initializeApp, getApp, getApps } from "firebase/app"
import { getAnalytics } from "firebase/analytics"
import { getFirestore } from "#firebase/firestore"
import { getStorage } from "#firebase/storage"
import getFirebaseObject from "./firebaseConfig"
const app = !getApps.length ? initializeApp(getFirebaseObject()) : getApp()
const DB = getFirestore(app)
const storages = getStorage()
const analytics = getAnalytics(app)
export { app, DB, analytics, storages }
as you see
const user = {
id: 1,
usename: "j",
password: "123456789",
}
in fact except of these static data wanna search and get right user info from the Firebase
I know there are a some other way of doing this but I like working with next-auth for last change wanna make sure there's a spot of light in this was ;)
i found this public repository where the author does something similar to what you want to achieve, which is create a custom token with your database credentials.
May be this repository can help you. It has a few errors, but it gave me a general idea about what to do, as I had a similar case.
try {
if (user !== null) {
await customTokenSignIn(user.id, user.email);
(await getUser(user.id)) ??
(await createUser(toReqUser(user, account)));
const data = await getUser(user.id);
setResUser(user, data as ResUser);
return true;
}
return false;
} catch (e) {
console.error(e);
return false;
}
const customTokenSignIn = async (id: string, email: string) => {
const hash = toHash(id);
const customToken = await adminAuth.createCustomToken(hash);
await auth.signInWithCustomToken(customToken).then((res) => {
res.user?.updateEmail(email);
});
await adminAuth.setCustomUserClaims(hash, { sid: id });
await createUserToken({ id: id, firebaseUid: hash });
};
Despite using a theoretically working authentication token, the userQuery request from UserProvider returns a null user and networkStatus of 7, indicating that it is done loading and that there is no "error," despite the user being null.
class UserProvider extends React.Component {
render() {
if (this.props.data.user) {
window.localStorage.setItem("userId", this.props.data.user.id)
}
if (this.props.data.loading) {
return <div>Loading</div>
}
return this.props.children(this.props.data.user)
}
}
const userQuery = gql`
query {
user {
id
}
}
`
export default graphql(userQuery, {
options: { fetchPolicy: "network-only" },
})(UserProvider)
I am setting up the network interface with this code.
const wsClient = new SubscriptionClient(
`wss://subscriptions.graph.cool/v1/redacted`,
{
reconnect: true,
}
)
const networkInterface = createNetworkInterface({
uri: "https://api.graph.cool/simple/v1/redacted",
})
const networkInterfaceWithSubscriptions = addGraphQLSubscriptions(
networkInterface,
wsClient
)
networkInterfaceWithSubscriptions.use([
{
applyMiddleware(req, next) {
if (!req.options.headers) {
req.options.headers = {}
}
console.log("applying middleware")
// get the authentication token from local storage if it exists
if (localStorage.getItem("auth0IdToken")) {
console.log("apply header", localStorage.getItem("auth0IdToken"))
req.options.headers["Authorization"] = `Bearer ${localStorage.getItem(
"auth0IdToken"
)}`
}
next()
},
},
])
let client = new ApolloClient({
networkInterface: networkInterfaceWithSubscriptions,
})
The user query will return null in your situation, when there is no User node in your Graphcool project where auth0UserId corresponds with the Auth0 id embedded in the JWT.
Please paste your token to https://jwt.io/ and check if a user exists with an id that is printed there. The Auth0 id starts with the auth provider, for example google-oauth2|<google id> for Google.