import Cookies from 'universal-cookie';
const cookies = new Cookies();
cookies.remove('abc');
console.log(cookies.getAll());
It is still printing my abc cookie.
May be you need to do something like
cookies.remove('abc', { path: '/' });
More info here
Cookies need to have both path and domain appended to them to be removed. Try this:
cookies.remove("abc", {path: "/", domain: ".example.com"})
If you are setting the cookie on a response in a login route/controller in express backend for JWT and are using 'httpOnly' option, you are unable to access the token from the client/react, even when using a third party library like 'universal-cookie' or 'document.cookie'.
You will need to clear the cookie on the response from the backend e.g. when a user logs out in the logout controller as detailed below.
Front-end:
// React redux logout action
export const logout = () => async (dispatch) => {
try {
await axios.get('/api/auth/logout')
localStorage.removeItem('userInfo')
dispatch({ type: type.USER_LOGOUT })
} catch (error) {
console.log(error)
}
}
Backend:
const User = require('../../models/userModel')
const generateToken = require('../../utils/generateToken')
// #desc Auth user & get token
// #route POST /api/auth/login
// #access Public
const login = async (req, res) => {
const { email, password } = req.body
try {
const user = await User.findOne({ email })
if (user && (await user.verifyPassword(password))) {
let token = generateToken(user._id)
res.cookie('token', token, {
maxAge: 7200000, // 2 hours
secure: false, // set to true if your using https
httpOnly: true,
})
res.json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token: token,
})
} else {
res
.status(401)
.json({ success: false, message: 'Invalid email or password' })
}
} catch (error) {
res.status(500).json({ success: false, message: error.toString() })
}
}
// #desc Logout controller to clear cookie and token
// #route GET /api/auth/login
// #access Private
const logout = async (req, res) => {
// Set token to none and expire after 1 seconds
res.cookie('token', 'none', {
expires: new Date(Date.now() + 1 * 1000),
httpOnly: true,
})
res
.status(200)
.json({ success: true, message: 'User logged out successfully' })
}
module.exports = {
login,
logout,
}
I just add this for people who may have similar problem in future, just like I had today. This may be an issue with asynchronous actions. Setting, removing cookies is asynchronous.
Related
I've followed a guide on Medium on the implementation of Passport js with a MERN stack and whilst I got authentication to work, I'm struggling to persist users between routes. Below are snippets of my code:
Backend
Server.js setup (part of it):
const mongoose = require('mongoose')
const MongoStore = require('connect-mongo')
const passport = require('./passport/setup')
const MONGO_URI = 'db'
const app = express();
mongoose
.connect(MONGO_URI, { useNewUrlParser: true })
.then(console.log(`MongoDB connected ${MONGO_URI}`))
.catch(err => console.log(err))
app.set("")
const PORT = process.env.PORT || 5000;
if (PORT !== 5000) {
app.use(enforce.HTTPS({ trustProtoHeader: true }));
}
app.use(cors({
origin: true,
credentials: true,
}))
app.options('*', cors())
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
store: MongoStore.create({ mongoUrl: MONGO_URI })
})
);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(passport.initialize());
app.use(passport.session());
Passport.js Local Strategy setup:
const bcrypt = require('bcryptjs');
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const User = require('../models/User')
passport.serializeUser((user, done) => {
done(null, user.id);
})
passport.deserializeUser((id, done) => {
User.findById(id, (err, user) => {
done(err, user);
});
});
passport.use(new LocalStrategy({ usernameField: "email" }, (email, password, done) => {
User.findOne({ email: email })
.then(user => {
if (!user) {
return done(null, false, { message: 'No User Found' })
} else {
bcrypt.compare(password, user.password, (err, isMatch) => {
if (err) throw err;
if (isMatch) {
return done(null, user);
} else {
return done(null, false, { message: "Wrong Password" });
}
});
}
})
.catch(err => {
return done(null, false, { message: err });
});
})
);
module.exports = passport;
Login route:
router.post("/new-login", async (req, res, next) => {
passport.authenticate("local", function (err, user, info) {
if (err) {
return res.status(400).json({ errors: err });
}
if (!user) {
return res.status(400).json({ errors: "No user found" });
}
req.logIn(user, function (err) {
if (err) {
return res.status(400).json({ errors: err });
}
return res.status(200).json({ success: `logged in ${user.id}` });
});
})(req, res, next);
})
Frontend React:
Posting Login Data:
export const postData = async (url, data) => {
try {
console.log('posting this ', data)
const config = {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'backend',
'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE'
}
}
const response = await axios.post(url, data, config);
console.log('getting this', response)
return {
data: response.data,
error: "",
success: true,
};
} catch (exp) {
console.log('Error', exp)
return {
success: false,
error: exp.response.data,
};
}
};
Axios call:
let response = await postData('/login/new-login', { email, password })
When trying to login all of this works and returns the login success message with user.id from the login route, but when I look at the Axios response there's no reference to any user or session as shown below:
I'm probably not understanding how users are passed to React from the backend but shouldn't res.user exist here following req.LogIn in Passport? Or should I take the user.id and save it in a global React state and attach it every time I do a request to a protected route? Right now if I do a GET request to a protected route I get a req.user undefined message.
Frontend Axios call:
export const getData = async (url) => {
console.log('not called?')
try {
const config = {
withCredentials: true,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': 'backend',
'Access-Control-Allow-Methods': 'GET,PUT,POST,DELETE'
}
}
const response = await axios.get(url, config);
console.log('response? ', response)
return {
data: response.data,
error: "",
success: true,
};
} catch (exp) {
return {
success: false,
error: exp.response.data,
}
}
}
Backend Protected Route:
router.get("/new-protected", passport.authorize('local', { failureRedirect: '/login' }), (req, res) => {
console.log('whats request?', req.user)
res.send(`Hello, logged in`);
})
When trying this I remove the middleware to see what req looks like, but I always get undefined req.user, so I'm guessing either I'm supposed to be passing req.user somehow in my axios call or I'm not retrieving a user when logging in initially, any ideas on what I'm doing wrong?
Many thanks
You don't have to do anything about the user's data once you log in with authorization, the passport middleware is here for the job, however we should at least set up the right condition for them to exchange the credential, try to change the setting a bit to see how it go :
this part :
app.use(cors({
origin: //put the EXACT URL you host from Vercel
method: 'GET,PUT,POST,DELETE'
credentials: true,
}))
app.set('trust proxy', 1)
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
store: MongoStore.create({ mongoUrl: MONGO_URI })
})
);
app.use(express.json()); // optional: you can uninstall bodyParser since express have now his own module so you can just replace with this line.
app.use(passport.initialize());
app.use(passport.session());
and update a bit this part at the frontend Axios call :
const config = {
withCredentials: true,
};
I don't know if you have already the issue in localhost or after deployment, if it sill not working than we may have to fix on the cookie session.
I'm making a website and on the login page there is an error that when the user tries to log in from a mobile phone, the request switches from POST to GET
In the frontend I added this function to make the request:
import axios from "axios"
const api = axios.create({
baseURL: "https://--------.herokuapp.com",
withCredentials: true
})
export default API
async function login() {
await api.post('user/login', {
username: username,
password: password
}).then(res => {
Redirect('/')
}).catch(err => {
const loginError = document.querySelector('#loginError')
loginError.style = "color: red;"
loginError.innerHTML = err.response.data
})
}
In the backend I added this code:
const router = require('express').Router()
//Login
router.post('/login', async (req, res) => {
//Check if the user is already in the DB
const user = await User.findOne({ username: req.body.username })
if (!user) return res.status(400).send('Invalid username or password')
//Check if the password is correct
const validPass = await bcrypt.compare(req.body.password, user.password)
if (!validPass) return res.status(400).send('Invalid username or password')
//Create and assign a token
if (user.admin) {
const token = jwt.sign({ user: user }, process.env.TOKEN_ADMIN_SECRET, {
expiresIn: 108000, // 30 hours
})
res.cookie('adminToken', token, {
httpOnly: true,
sameSite: "none",
secure: true,
maxAge: 30 * 60 * 60 * 1000, // 30 hours
}).send("Logged In")
} else {
const token = jwt.sign({ user: user }, process.env.TOKEN_SECRET, {
expiresIn: 108000, // 30 hours
})
res.cookie('authToken', token, {
httpOnly: true,
sameSite: "none",
secure: true,
maxAge: 30 * 60 * 60 * 1000, // 30 hours
}).send("Logged In")
}
})
And on my phone I get this error:
Should be POST not GET
Details of the request
UPDATE
It works now. All I had to change is
session: {
jwt: true,
strategy: "jwt"
}
I am new in next-auth. I was practicing next-auth sign-in with credentials using MongoDB as a database. Whenever I sign in with credentials, useSession doesn't return data as expected but if sign in with other providers like google and GitHub it returns data as expected. Below I have given my code.
[...nextauth.js] code:-
export default NextAuth({
// Configure one or more authentication providers
adapter: MongoDBAdapter(clientPromise),
session: {
jwt: true,
},
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
CredentialsProvider({
async authorize(credentials) {
const client = await connect();
const usersCollection = client.db().collection("users");
const user = await usersCollection.findOne({
email: credentials.email,
});
if (!user) {
client.close();
throw new Error("No user found!");
}
const isValid = bcrypt.compare(credentials.password, user.password);
if (!isValid) {
client.close();
throw new Error("Invalid password/email");
}
client.close();
return user;
},
}),
// ...add more providers here
],
callbacks: {
async session({ session, user, token }) {
session.user = token.user;
console.log(user)
return session
},
async jwt({ token, user }) {
if (user) {
token.user = user;
}
// console.log(token)
return token
},
},
});
signIn logic:
const handleSubmit = async (e) => {
e.preventDefault();
const status = await signIn("credentials", {
redirect: false,
email: emailRef.current.value,
password: passwordRef.current.value,
});
console.log(status); // {error: null, status: 200, ok: true, url: 'http://localhost:3000/login'}
console.log(session); // {data: null, status: 'unauthenticated'}
};
Sorry for my bad English.
Thanks in advance. :)
I can't get the token with getToken:
This variables are ok:
NEXTAUTH_SECRET=secret
NEXTAUTH_URL=http://localhost:3000
Here is my [...nextauth].js - I can do console.log(token) and it works well
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
...
jwt: {
secret: process.env.JWT_SECRET,
encryption: true,
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async redirect({ url, baseUrl }) {
return Promise.resolve(url);
},
async jwt({ token, user, account, profile, isNewUser }) {
return token;
},
async session({ session, user, token }) {
return session;
},
},
});
API section (I think getToken doesnt work well):
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async (req, res) => {
const token = await getToken({ req, secret, encryption: true });
console.log(token);
if (token) {
// Signed in
console.log("JSON Web Token", JSON.stringify(token, null, 2));
} else {
// Not Signed in
res.status(401);
}
res.end();
};
This might be a bug in the function but the only way i got it to work was to use getToken to get the raw jwt token and then use jsonwebtoken package to verify and decode it
import { getToken } from "next-auth/jwt";
import jwt from "jsonwebtoken";
const secret = process.env.NEXT_AUTH_SECRET;
const token = await getToken({
req: req,
secret: secret,
raw: true,
});
const payload = jwt.verify(token, process.env.NEXT_AUTH_SECRET);
console.log(payload);
this worked for me:
const secret = process.env.SECRET
const token = await getToken({ req:req ,secret:secret});
also check [...nextauth].js, if you're using
adapter: MongoDBAdapter(clientPromise),
then you dont have JWT token, you have to set
session: {
strategy: "jwt",
},
notice this will save the session locally and not on the database
more info here: https://next-auth.js.org/configuration/options#session
This issue still exists, but unlike other answers, I simply had to pass the NEXTAUTH_SECRET that I had set on my .env file to my getToken() function without setting raw: true.
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async (req, res) => {
const token = await getToken({ req: req, secret: secret });
if (token) {
// Signed in
console.log("JSON Web Token", JSON.stringify(token, null, 2));
} else {
// Not Signed in
res.status(401);
}
res.end();
};
I am adding JWT authentication to a blog app I'm working on. On the server side (built with Nodejs) I am creating the token and sending it back with a successful login. On the client side I am saving the token in LocalStorage. When I log in and check the application tab in dev tools I can see the token. On the server route where blogs are posted to I check authentication. If the token is authenticated the blog posts to the database, but if I delete the token or change it and then make the post request the request fails, as expected.
So far so good.
What I'm confused about is how to restrict access to the page where the blog editor resides on the client. If people aren't authenticated they should not be able to access this page at all, even though if not authenticated they can't post anyway.
Login route on server:
router.post('/login', async (req, res, next) => {
const cursor = User.collection.find({username: req.body.username}, {username: 1, _id: 1, password: 1});
if(!(await cursor.hasNext())) {
return res.status(401).json({ message: 'Cannot find user with that username' });
}
const user = await cursor.next();
try {
if(await bcrypt.compare(req.body.password, user.password)) {
const token = jwt.sign({
email: user.email,
userId: user._id
}, process.env.JWT_SECRET, { expiresIn: "1h" })
return res.status(201).json({
message: 'User Authenticated',
token: token
});
} else {
return res.status(400).json({
authenticated: false,
username: req.body.username,
password: req.body.password
})
}
} catch (err) {
return res.status(500).json({ message: err })
}
});
How I'm checking the token authentication on the server:
const jwt = require('jsonwebtoken');
module.exports = (req, res, next) => {
try {
const token = req.headers.authorization;
console.log(token);
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userData = decoded;
next();
} catch (error) {
return res.status(401).json({ message: 'Auth Failed' })
}
}
My client side login route fetch:
handleSubmit(event) {
event.preventDefault();
const formData = {
username: event.target.username.value,
password: event.target.password.value
}
fetch('http://localhost:4000/user/login', {
method: "POST",
mode: "cors",
body: JSON.stringify(formData),
headers: {
"Content-Type": "application/json"
}
})
.then(res => res.json())
.then(res => {
localStorage.setItem('authorization', res.token);
console.log(res);
})
.catch(err => console.error(err))
}
And here is my fetch call from the client on the blog posting route where the editor resides:
handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.target);
const body = event.target.postBody.value;
const postTitle = event.target.title.value;
console.log(event.target);
console.log(data);
console.log(event.target.postBody.value);
fetch('http://localhost:4000/blog', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
"Authorization": localStorage.getItem('authorization')
},
mode: 'cors',
body: JSON.stringify({
title: postTitle,
postBody: body
})
})
.then(res => res.json())
.then(err => console.error(err))
}
So, like I said, everything is working as expected but I don't want people to be able to access the editor page if they are not authenticated. I guess I would check to see if the token exists in localstorage and then redirect? But wouldn't I also need to check to see if the token on the client can be authenticated on the server as well? So would I essentially need to post to the server to do the check whenever someone navigates to that page, or any other page I want to restrict access to? Come to think of it, if a user is already authenticated I don't want them to be able to access the login page either.
I have heard that people use Redux to manage state across components, but I really don't want to go down that road, at least not yet because this project is for learning purposes and I don't really want to start with Redux or anything else like that until I have a better grasp of React on it's own. I don't know if I need Redux or not and from what I understand, that's enough to know that I probably don't need it.
This is just such a different flow than I'm used to from PHP sessions and I'm having some trouble wrapping my head around it.
I realize that you folks may not really need to see all this code, but I also would like some more experienced eyes to see it and point out anywhere I might be making mistakes or where I could improve here.
So this is what I have come up with for now, if anyone knows a better way, I'm definitely open to suggestions.
I created a class called CheckAuth which essentially just makes a GET request to the server and sends the jwt along with it.
checkAuth.js:
class CheckAuth {
constructor() {
this.auth = false;
}
async checkLogin() {
console.log(localStorage.getItem("authorization"));
let data = await fetch('http://localhost:4000/auth', {
method: "GET",
mode: "cors",
headers: {
"Content-Type": "application/json",
"authorization": localStorage.getItem("authorization")
}
})
return data.json();
}
logout(cb) {
localStorage.removeItem('authenticated')
this.auth = false;
cb();
}
async isAuthenticated() {
const data = await this.checkLogin()
return data;
}
}
export default new CheckAuth();
Then on pages that only logged in users should see I am doing a simple check to see if they have the token and if it's valid inside of componentDidMount().
componentDidMount() {
const check = checkAuth.isAuthenticated();
console.log(check);
check.then(res => {
console.log(res);
if(res.authenticated !== true) {
this.props.history.push("/login");
}
})
.catch(err => { console.error(err) })
}