next-auth question about credentials provider - next-auth

next-auth credentials provider only has methods for signIn and signOut, which has led me to think I might be missing something about the purpose of the credentials provider in general.
shouldn't there also be a log in method?
The way it is, in order to do login and signup separately, I can only use signIn for both.
That would almost be ok because I could conditionally check if a user has an account inside of the CredentialsProvider method, and then login or signup conditionally.
The problem is that then the login form also works as a sign up form if the user were to to enter the wrong username for instance... it would just see that account doesn't exist and sign them up.
Obviously that is very bad.
Here is the CredentialsProvider method that gets called when you use 'signIn'
export default CredentialsProvider({
name: '',
credentials: {
email: { label: 'Username', type: 'text', placeholder: 'email' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid Credentials')
}
//conditionally query or signup mutation here depending on if the credentials exists
const signIn = UserOperations.Mutations.signIn
await client.query<SignUpResponse, SignUpInput>({
query: signIn,
variables: {
email: credentials.email,
password: credentials.password
}
})
const signUp = UserOperations.Mutations.signUp
const response = await client.mutate<SignUpResponse, SignUpInput>({
mutation: signUp,
variables: {
email: credentials.email,
password: credentials.password
}
})
const { data } = response
const user = data?.signUp
if (user) {
return user
}
return null
}
})
And you then you call it from your login component or sign up component like this:
await signIn('credentials', {
redirect: false,
email,
password,
callbackUrl: `${window.location.origin}`
})
But I don't see any way to distinguish where it's being called from. Isn't the point of credentials to be able to have login and sign up separate?

I figured it out. The idea is to use CredentialsProvider twice, once for signup and once for login.
You can specify which provider gets called with the id property in the provider.
const signUp = CredentialsProvider({
id: 'sign-up-provider',
credentials: {
email: { label: 'Username', type: 'text', placeholder: 'email' },
password: { label: 'Password', type: 'password' }
},
//authorize will be called whether this is signUp or signIn
async authorize(credentials, req) {
if (!credentials?.email || !credentials?.password) {
throw new Error('Invalid Credentials')
}
const signUp = UserOperations.Mutations.signUp
const response = await client.mutate<SignUpResponse, SignUpInput>({
mutation: signUp,
variables: {
email: credentials.email,
password: credentials.password
}
})
const { data } = response
const user = data?.signUp
if (user) {
return user
}
return null
}
})
export default signUp
and then instead of passing in 'credentials' to signIn you can pass in the custom id.
await signUp('sign-up-provider', {
redirect: false,
email,
password,
callbackUrl: `${window.location.origin}`
})
Do the same for a login provider, and then add both providers in the nextAuth providers array.

Related

Login post with Bcrypt always return false

Bcrypt.compare returns false no matter what
I'm currently working on a login/register feature in NextJS that utilizes bcrypt to hash and compare user passwords. I'm able to register a user with a hashed password, but when attempting to log in with bcrypt.compare(), the comparison always returns false, even when the entered password matches the hashed password.
The issue lies in this line: const isPasswordMatched = await bcrypt.compare(password, user.password);, where the compare() method is used to compare the entered password with the hashed password. Despite the method's implementation, it's not working as expected.
api/auth/[...nextauth].ts for login
const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
async authorize(credentials, req) {
await connectDB();
const { email, password }: Icredential = credentials;
// Find user by email
const user = await User.findOne({ email: email });
if (user === null) {
throw new Error('Cannot find user');
}
// Check if password from input matches with the one from db
// This is the line of the concern
const isPasswordMatched = await bcrypt.compare(password, user.password);
console.log(`Comparing ${password} to ${user.password}`);
console.log("match ?", isPasswordMatched);
// Throw error when it doesn't
if (!isPasswordMatched)
// if (password !== '123')
{
throw new Error('Invalid email or password');
}
// Return authorized user
return user;
},
credentials: undefined
}),
],
};
export default NextAuth(authOptions);
api/register for register
const registerHandler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") {
try {
const { user: _regiUser } = req.body;
console.log(_regiUser)
//Check if user exists
await connectDB()
const existingUser = await User.findOne({ email: _regiUser.email }).exec();
console.log("existingUser", existingUser);
//Throw error when email is already in use
if (existingUser) {
throw new Error("Email already used");
}
//Password encrypted
const hashedPassword: string = await bcrypt.hashSync( _regiUser.password, 10 );
console.log("_regiUser.password", _regiUser.password, hashedPassword)
console.log(hashedPassword)
//Replace text password with encrypted password
_regiUser.password = hashedPassword;
console.log(_regiUser)
//Add user on database
await User.create(_regiUser)
res.end()
} catch (e: any) {
console.log(e.message)
}
}
};
export default registerHandler;
Login logic was completely correct, but I had
wrong User model like following:
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true,
lowercase: true
},
password: {
type: String,
required: true,
lowercase: true //remove this to make it work
}
});
look at password entity, because I copy pasted from email entity, i had a wrong configuration for password. So hash stored in lowercase and this is the very reason why i got error no matter what. smh...
You're creating your password hashes using the hashSync() method (not async) but trying to run the async .compare() method when logging-in. Check out the examples.
For the comparison, you should be using:
bcrypt.compareSync(myPlaintextPassword, hash);
Otherwise, I recommend using the async/await bcrypt.hash and bcrypt.compare methods. If you want to use await bcrypto.compare(...), create your hash using:
await bcrypt.hash(password, 10);

NEXT JS AND MONGODB JWT integration

Looking for a backend dev that can simply help me implement MONGODB with nextJS and the current model I have now. I have bought https://www.devias.io admin dashboard, and just want to implement auth and database reading with it.
Just want the basic auth setup. It's already setup in the FILES just wanting to know how to configure it properly based on the devias guides
Has anyone done this before I can't find any documentation on it
It's setup with mock data at the moment
SRC/API/AUTH/index.js
import { createResourceId } from '../../utils/create-resource-id';
import { decode, JWT_EXPIRES_IN, JWT_SECRET, sign } from '../../utils/jwt';
import { wait } from '../../utils/wait';
import { users } from './data';
class AuthApi {
async signIn(request) {
const { email, password } = request;
await wait(500);
return new Promise((resolve, reject) => {
try {
// Find the user
const user = users.find((user) => user.email === email);
if (!user || (user.password !== password)) {
reject(new Error('Please check your email and password'));
return;
}
// Create the access token
const accessToken = sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
resolve({ accessToken });
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
async signUp(request) {
const { email, name, password } = request;
await wait(1000);
return new Promise((resolve, reject) => {
try {
// Check if a user already exists
let user = users.find((user) => user.email === email);
if (user) {
reject(new Error('User already exists'));
return;
}
user = {
id: createResourceId(),
avatar: undefined,
email,
name,
password,
plan: 'Standard'
};
users.push(user);
const accessToken = sign({ userId: user.id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
resolve({ accessToken });
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
me(request) {
const { accessToken } = request;
return new Promise((resolve, reject) => {
try {
// Decode access token
const { userId } = decode(accessToken);
// Find the user
const user = users.find((user) => user.id === userId);
if (!user) {
reject(new Error('Invalid authorization token'));
return;
}
resolve({
id: user.id,
avatar: user.avatar,
email: user.email,
name: user.name,
plan: user.plan
});
} catch (err) {
console.error('[Auth Api]: ', err);
reject(new Error('Internal server error'));
}
});
}
}
export const authApi = new AuthApi();
then /SRC/API/AUTH/data.js
export const users = [
{
id: '5e86809283e28b96d2d38537',
avatar: '/assets/avatars/avatar-anika-visser.png',
email: 'demo#devias.io',
name: 'Anika Visser',
password: 'Password123!',
plan: 'Premium'
}
];
This is the documentation on it
JSON Web Token (JWT)
Most auth providers use this strategy under the hood to provide access tokens. Currently, the app doesn't cover the backend service, and this service is mocked (faked) using http client interceptors. The implementation is basic, but enough to give you a starting point.
How it was implemented
Since tokens are meant to be created on the backend server, they are built with encrypt, encode and decode utility methods because they are not meant to be used on the client. These utilities can be found in src/utils/jwt. These are for development purposes only, and you must remove (or avoid using) them.
How to use JWT Provider
The app is delivered with JWT Provider as default auth strategy. If you changed or removed it, and you want it back, simply follow these steps:
Step 1: Import the provider
Open src/pages/_app.js file, import the provider and wrap the App component with it.
// src/pages/_app.js
import { AuthConsumer, AuthProvider } from '../contexts/auth/jwt-context';
const App = (props) => {
const { Component, pageProps } = props;
return (
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
);
};
Step 2: Set the hook context
Open src/hooks/use-auth.js file and replace the current context the following line:
import { AuthContext } from '../contexts/auth/jwt-context';
How to use auth
Retrieve user profile
In the example below, you can find how it can be used in any component not just the App. Should you want to use it in any other component, you'll have to import the useAuth hook and use it as needed.
// src/pages/index.js
import { useAuth } from '../hooks/use-auth';
const Page = () => {
const { user } = useAuth();
return (
<div>
Email: {user.email}
</div>
);
};
Auth methods / actions
For simplicity and space limitations, the code below is used only to exemplify, actual code can be found in the components.
// src/pages/index.js
import { useAuth } from '../hooks/use-auth';
const Page = () => {
const { login } = useAuth();
const handleLogin = () => {
// Email/username and password
login('demo#devias.io', 'Password123!');
};
s
return (
<div>
<button onClick={handleLogin}>
Login
</button>
</div>
);
};
Implemented flows
Currently, the app only covers the main flows:
Register
Login
Logout
const mongoose = require('mongoose');
const jwt = require("jsonwebtoken");
// Connect to MongoDB
mongoose.connect('mongodb://localhost/yourdbname', {
useNewUrlParser: true,
useUnifiedTopology: true
});
const userSchema = new mongoose.Schema({
id: {
type: String,
required: true,
unique: true
},
email: {
type: String,
required: true
},
name: {
type: String,
required: true
},
password: {
type: String,
required: true
},
plan: {
type: String,
default:
'Standard'
},
avatar: {
type: String,
default:
null
},
});
const User = mongoose.model('User', userSchema);
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
class AuthApi {
async signIn(request) {
const {
email,
password
} = request;
const user = await User.findOne({
email
});
if (!user || (user.password !== password)) {
throw new Error('Please check your email and password');
}
const accessToken = jwt.sign({
userId: user.id
}, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
return {
accessToken
};
}
async signUp(request) {
const {
email,
name,
password
} = request;
const existingUser = await User.findOne({
email
});
if (existingUser) {
throw new Error('User already exists');
}
const newUser = new User({
id: mongoose.Types.ObjectId(),
email,
name,
password,
plan: 'Standard',
avatar: null,
});
await newUser.save();
const accessToken = jwt.sign({
userId: newUser.id
}, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN
});
return {
accessToken
};
}
async me(request) {
const {
accessToken
} = request;
const decoded = jwt.verify(accessToken, JWT_SECRET);
const {
userId
} = decoded;
const user = await User.findById(userId);
if (!user) {
throw new Error('Invalid authorization token');
}
return {
id: user.id,
avatar: user.avatar,
email: user.email,
name: user.name,
plan: user.plan
};
}
}
export const authApi = new AuthApi();

How to get data from next auth signIn Google provider in custom signIn page?

I need to get the data from the custom signIn page in order to write a user to the sanity database. But these signIn data is only obtained in [...nextauth].js file.
Code:
[...nextauth].js
import NextAuth from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: "xxxxxx",
clientSecret: "xxxxx",
}),
// ...add more providers here
],
secret: "something",
pages: {
signIn: '/auth/signin',
},
callbacks: {
async session({ session, token, user }) {
session.user.username = session.user.name
.split(' ')
.join('')
.toLocaleLowerCase()
session.user.uid = token.sub
return session
},
},
})
And these session data can be used inside components using useSession from next-auth.
But while trying to get the data to my custom signIn page, session is undefined.
import { getProviders, signIn as signIntoProvider } from "next-auth/react";
import { sanityClient } from "../../sanity";
import { useSession } from 'next-auth/react';
function signIn({users}) {
const { data: session } = useSession()
const onSubmit = (data) => {
fetch("/api/createUser", {
method: "POST",
body: JSON.stringify(data),
})
.then((resp) => {
console.log(resp);
})
.catch((err) => {
console.log(err);
});
};
const checkIfUserThere = async () => {
let newUser = session.user.email;
for (const data of users) {
if (data.email === newUser) {
return true;
}
}
return false;
};
useEffect(() => {
(async () => {
const userExists = await checkIfUserThere();
if(userExists === false ){
onSubmit(session.user); //write to sanity database
}
})();
}, []);
return (
<>
<button
className="rounded-lg bg-blue-500 p-3 text-white"
onClick={() =>
signIntoProvider("google", { callbackUrl: "/" })
}
>
Sign in with Google
</button>
</>
);
}
The above code is for a custom signIn page.
What is expected :
Once the user clicks the sign-in with the Google button, the session data must be added to the sanity database. But in my case, session here is undefined.
A simple way to do this is to write the logic inside the [...nextAuth].js file.
To solve the task of popularizing a document in Sanity from a Google authentication, you must first establish a connection to your Sanity project. Note that this import comes from the 'npm i #sanity/client' package or 'yarn add #sanity/client' and is not a reference to the configuration located in the sanity.js file. To do this, you can import the #sanity/client library and set up a configuration to connect to your project:
import sanityClient from "#sanity/client";
const config = {
dataset: "DATASET_NAME",
projectId: "PROJECT_ID",
useCdn: 'CDN'
token: "YOUR_TOKEN_SANITY",
};
export const client = sanityClient(config);
After setting up authentication with Google, you must set up a callback to run every time a user authenticates. This callback should look in Sanity to see if the user already exists, and if not, create a new user document in Sanity with the authenticated user's information:
const populateSanityUser = async (user) => {
const sanityUser = await client.fetch(
`*[_type == "users" && email == "${user.email}"]{ //check if the email exists
email
}`
);
if (sanityUser.length > 0) { //if exists
return sanityUser;
} else { //if not, create a new user with Google user session data
try {
await client.create({
_type: "user",
name: user.name,
email: user.email,
urlImage: user.image,
... // another field in your document
});
return user;
} catch (error) {
return error;
}
}
};
export default NextAuth({
...authOptions,
callbacks: {
async signIn(user) {
const isAllowedToSignIn = true; //optional
if (isAllowedToSignIn) {
const sanityUser = await populateSanityUser(user.user);
return sanityUser;
} else {
return false;
}
},
},
});
Important: Make sure that when you pass the user coming from NextAuth callback function you use user.user, as it comes nested with more data.
More information about callbacks in NextAuth here: https://next-auth.js.org/configuration/callbacks

Mongo DB .findOne returning null value. This also lead to my login not working

My current login route function that use findOne. Currently using findOne with a email being used to searched for inside my database. But since my user always return null my feedback is always Invalid credentials.
const login = asyncHandler(async (req, res) => {
const { email, password } = req.body
// Check for user email
const user = await Staff.findOne({email})
console.log(user);
if (user && (await bcrypt.compare(password, user.password))) {
res.json({
_id: user.id,
name: user.name,
email: user.email,
})
} else {
res.status(400)
throw new Error('Invalid credentials')
}
})

Next-Auth with Provider.Credentials: How to implement when API is already returning a JWT Token?

I have a NextJS page where I try to implement Next-Auth.
I use credentials to login to my Rails API.
My API is returning (already) a JWT-Token. (so NextAuth must not create it)
How to implement the Provider.Credentials for [...nextauth].js in that case?
Flow "Diagram"
Next request ---> Next API (with Next-Auth) ---> Rails API (returning Token)
At the momemt I have these options:
providers: [
CredentialsProvider({
name: 'Email',
credentials: {
email: { label: "Email", type: "email", placeholder: "meine.email#domain.com" },
password: { label: "Passwort", type: "password" }
},
async authorize(credentials) {
// The 'url' is pointing to a Rails API endpoint which returns a JWT Token
const url = `${process.env.NEXT_PUBLIC_API_URL}/auth/login`;
const res = await fetch(url, {
method: 'POST',
body: JSON.stringify(credentials),
headers: {
"Content-Type": "application/json" }
})
const user = await res.json()
// If no error and we have user data, return it
if (res.ok && user) {
// I SAW EXAMPLES RETURNING {"email": "blah#tst.com"}
return user // MY CONTENT {token: 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJyb2xl…0.OAGiwjj9O_NsH02lIjA2D4HYZkmTQ3_SqtKcVgaIul0'}
}
// Return null if user data could not be retrieved
return null
}
})
]
}
A session_token is set in the browser, but that content is something (random?) what I dont have set. Where does this content come from if not from my token?
My Rails API Token Content:
{
"user_id": 4,
"roles": [
"user"
],
"exp": 1631096219
}
Next-Auth API Token Content:
{
"iat": 1631009819,
"exp": 1633601819
}
Do I have to decode my API token and reassamble that within the Provider.Credentials function?
I implement Next-Auth to provide more Authentications like Twitter and Co, but as well to make use of "useSession" instead of building everything of my own (Wont reinventing the wheel).
Add Callbacks.
export default NextAuth({
providers: [
CredentialsProvider({
name: "Email",
credentials: {
email: {
label: "Email",
type: "email",
placeholder: "meine.email#domain.com",
},
password: { label: "Passwort", type: "password" },
},
async authorize(credentials) {
// The 'url' is pointing to a Rails API endpoint which returns a JWT Token
const url = `${process.env.NEXT_PUBLIC_API_URL}/auth/login`;
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(credentials),
headers: {
"Content-Type": "application/json",
},
});
const user = await res.json();
// If no error and we have user data, return it
if (res.ok && user) {
// I SAW EXAMPLES RETURNING {"email": "blah#tst.com"}
return user; // MY CONTENT {token: 'eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJyb2xl…0.OAGiwjj9O_NsH02lIjA2D4HYZkmTQ3_SqtKcVgaIul0'}
}
// Return null if user data could not be retrieved
return null;
},
}),
],
callbacks: {
async jwt({ token, user, account, isNewUser }) {// This user return by provider {} as you mentioned above MY CONTENT {token:}
if (user) {
if (user.token) {
token = { accessToken: user.token };
}
}
return token;
},
// That token store in session
async session({ session, token }) { // this token return above jwt()
session.accessToken = token.accessToken;
//if you want to add user details info
session.user = { name: "name", email: "email" };//this user info get via API call or decode token. Anything you want you can add
return session;
},
},
});
So the user that you return from the credentials authorize function will get stored in the JWT (JSON Web Token - which is passed around with all authenticated requests) which is a hashed/encoded (not encrypted by default!) version of the object.
You can use https://jwt.io to debug and see whats in your JWT. You paste into that site's debugger field your eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjo0LCJyb2xl... value and it'll show you the { iat: 49835898: sub: 47897: email: 'abc#xyz.com' }, etc. value.
If your rails API is returning a JWT to you in that fetch call in the authorize function, you can use a library like jwt-decode to decode it right there, and append any values from it which you would like to "persist" in your application to the user object you're returning from the authorize function in next-auth.