Express.js backend not saving cookie to Nuxt.js frontend - javascript

I've built an authorization server with Express.js that works when testing with Postman where it saves the access and rotating refresh token as signed cookies.
However, what I completely forgot, was that my backend and frontend are two completely separate servers and domains. I tried enabling the Access-Control-Allow-Credentials, but that didn't work. I don't get any error messages, the cookies simply aren't being saved.
Here is my login controller from the backend:
const userLogin = async (req, res, next) => {
const { email, password } = req.body;
if (!email || !password) return res.sendStatus(400);
try {
const login = await loginUser(email, password);
if (login == 'wrong_email_or_password') return res.status(200).json({ error: login });
res.header('Access-Control-Allow-Credentials', true);
res.cookie('access', login.accessToken, { httpOnly: true, secure: (env != 'dev'), signed: true });
res.cookie('refresh', login.refreshToken, { httpOnly: true, secure: (env != 'dev'), signed: true });
res.status(200).json(login);
} catch(e) {
console.log(e.message);
res.sendStatus(500) && next();
}
}
And after I encountered some CORS issues, I also did app.use(cors({ origin: true, credentials: true }));, which resolved the issue. For production I'll probably also add a CORS-whitelist.
And here's my Nuxt.js method that will be called upon submitting the register-form:
async register () {
try {
if (!(this.firstname && this.lastname && this.username && this.email && this.password)) throw new Error('Pflichtfelder ausfüllen')
const res = await axios.post(`${this.$axios.defaults.baseURL}register`, {
firstname: this.firstname,
lastname: this.lastname,
username: this.username,
email: this.email,
password: this.password
}, {
withCredentials: true,
credentials: 'include'
})
console.log(res)
this.$router.go()
} catch (e) {
this.error = e.message
console.error(e)
}
}
I wish I could provide some more helpful information other than "doesn't work, help pls", but as I said, I don't get any errors whatsoever, so I really don't even know where to look. :(

Alright, it would seem that what I'm trying to do is impossible. Cookies coming from third-party domains are not allowed, which makes sense now that I think of it...
For anyone interested, my solution is to just save both tokens in localStorage. Since I am using rotating refresh tokens this isn't that big of a deal.

Related

Redirection when CORS credentials : true

I wrote a basic authentication code for logging a user in. The code basically generates a JWT token with a given set of params and registers a cookie onto the requesting client.
Now, I want to redirect the user to the last route.
I believe this is done using
res.redirect('back')
Yes, I am using express and React with react-router-dom
Here is the login code:
const login = async (req, res) => {
const { email, password } = req.body
const user = await Users.findOne({
email
})
if (user && user.id) {
// user was found
// check the password
const isValidPassword = bcrypt.compareSync(password, user.password)
if (isValidPassword) {
// generate a JWT token
const authToken = jwt.sign({
id: user.id,
name: user.name,
email: user.email,
created_at: user.created_at
}, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRES_IN
})
// log him in
// const response = {
// id: user.id,
// email: user.email,
// name: user.name,
// created_at: user.created_at
// }
res.cookie(COOKIE_TOKEN, authToken, {
maxAge: +process.env.JWT_EXPIRES_IN,
httpOnly: true
})/*.send(response)*/.redirect('back')
}
else {
res.status(401).send("Incorrect password")
}
}
else {
res.status(404).send({
status: "Failed",
reason: "Incorrect credentials or the user doesn't exist"
})
}
}
Now, the redirect call shows a CORS error on my React app.
Also note, I have used credentials: true for my cors configuration.
Here is the cors part of the code:
app.use(cors({
origin: 'http://localhost:3000',
credentials: true,
optionsSuccessStatus: 200,
}))
The credentials : true is required to establish server-side cookies and that is a must for my application.
Please give me an explanation as to why my redirect is not working and what are the changes required.
P.S. The login route is a POST request

What am I doing wrong with sessions?

so Ive finally deployed my app and resolved all the CORS issues, but I have a problem with user authentification. I can log-in, but when I refresh a site I get automaticly logged out -> on all browsers beside Mozilla Firefox, there it works somehow.
userContext.js -> Front-end
//XXX Login
const login = () => {
Axios.post(`${apiUrl}/users/login`, {
userName: nameLog,
userPassword: passwordLog,
}).then((response) => {
console.log(response);
if (!response.data.errors) {
setUser(response.data.name);
setUserId(response.data.user_id);
} else {
console.log(response);
const errors = response.data.errors;
console.log(errors);
processErrors(errors);
}
});
};
//Checking if user is logged in on every refresh of a page
useEffect(() => {
Axios.get(`${apiUrl}/users/login`).then((response, err) => {
console.log("GET /login RESPONSE: ", response);
if (response.data.loggedIn === true) {
setUser(response.data.user[0].name);
setUserId(response.data.user[0].user_id);
}
});
}, []);
First call is a POST request, thats when user logs in using form on my site.
And second one is a GET request, that checks if the session returns loggedIn true, this is called on every refresh of a page as it is inside useEffect hook.
Then I update my userState which acts as auth if user is allowed to do some action or not.
userRoutes.js -> Back-end
//Login user
router.post(
"/login",
[
check("userName").exists().notEmpty().withMessage("Username is empty.").isAlpha().isLength({ min: 3, max: 40 }),
check("userPassword").exists().notEmpty().withMessage("Password is empty.").isLength({ min: 3, max: 60 }).escape(),
],
(req, res) => {
const valErr = validationResult(req);
if (!valErr.isEmpty()) {
console.log(valErr);
return res.send(valErr);
}
const name = req.body.userName;
const password = req.body.userPassword;
const sql = "SELECT * FROM users WHERE name = ?";
db.query(sql, name, (err, result) => {
if (err) throw err;
if (!result.length > 0) {
res.send({ errors: [{ msg: "User doesn't exist" }] });
} else {
//compare hashed password from front end with hashed password from DB
bcrypt.compare(password, result[0].password, (error, match) => {
if (error) throw error;
//if passwords match send -> create session and send user data
if (match) {
req.session.user = result;
res.send({ user_id: result[0].user_id, name: result[0].name });
} else {
res.send({ errors: [{ msg: "Wrong username or password" }] });
}
});
}
});
}
);
//Checking if user is logged in and if so, sending user's data to front-end in session
router.get("/login", (req, res) => {
console.log("GET /login SESSION: ", req.session);
if (req.session.user) {
res.send({ loggedIn: true, user: req.session.user });
} else {
res.send({ loggedIn: false });
}
});
Again first one is for the POST request, where I create session and send it in response filled with user's data (name,id) to front-end (then I update my states accordingly).
Second one belongs to the GET request and returns false if user is not logged in or true + user's data. Then once again I update my states.
However this doesnt work and I dont know why. As I mentioned it returns loggedIn: false on every browser besides Mozzilla Firefox.
This is my first time dealing with sessions and cookies so what am I missing here?
By the way the site url is here if that helps: LINK, I left some console.logs() to display responses.
EDIT: adding all middleware
app.js -> main nodejs file
const express = require("express");
const bodyParser = require("body-parser");
const cookieParser = require("cookie-parser");
const session = require("express-session");
const cors = require("cors");
const { check, validationResult } = require("express-validator");
const userRoutes = require("./routes/userRoutes.js");
const app = express();
app.use(express.json());
app.use(
cors({
origin: [
"http://localhost:3000",
"https://todo-react-node.netlify.app",
],
methods: ["GET, POST, PATCH, DELETE"],
credentials: true, //allowing cookies
})
);
app.use(cookieParser());
app.use(bodyParser.urlencoded({ extended: true }));
app.use((req, res, next) => {
console.log("SESSION 2 : ", req.session);
console.log("Cookies 2 : ", req.cookies);
next();
});
app.use(
session({
key: "userID",
secret: "subscribe",
resave: false,
saveUninitialized: false,
})
);
app.use("/users", userRoutes);
Your useEffect only called once if you are using [] in useEffect function.
Try to take it out(or add the suitable dependencies) and add a console.log() in that function to test out.
Furthermore, read the following article to gain some insight about useEffect.
Okay so finally Ive managed to solve this issue with sessions and cookies. Solution at the end.
What was causing problems?
Cross domain cookies (probably). As Ive mentioned everything worked fine when Ive hosted my app on localhost, but as soon as Ive deployed my app to 2 different hostings (Netlify - front end, Heroku - back end) my cookies werent coming trough.
Ive actually managed to send cookie by
res.cookie("cookie_name", "cookie_value", {sameSite: "none" secure: true})
, however It was standalone cookie and not associated with a session.
Sure why dont you edit your settings in express-session then? Ive tried -> didnt work. My session was never created. I could only send standalone cookie.
And another problem I couldnt delete this cookie in browser after being set, nor I could modify it. Ive tried res.clearCookie("cookie_name") or setting maxAge attribute to -1, nothing worked.
After googling and watching a lot of videos I found out that sending and receiving cookies from different domains is restricted because of security.
SOLUTION -> How did I fix the problem?
Well Ive came upon a VIDEO on YouTube, that showed how to host full-stack application on Heroku. That way your front end and back end are on the same domain and voilà sessions and cookies are working properly. Basically it goes like this:
1) Build your React app
2) In /server (main back end folder) create /public folder and move content of your /client/build directory here
3) In your back end main file (app.js in my case) add app.use(express.static("public")); to serve your front end.
4) Change cors settings to:
app.use(
cors({
origin: "your Heroku app url",
credentials: true, //allowing cookies
}));
5) Change express-session settings
app.use(
session({
name: "name of session",
key: "your key",
secret: "your secret",
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 1000 * 60 * 60 * 24,
},
}));
6) Fix your routes for API calls (currently what Im working on)
7) Host it
Create session:
req.session.SESSION_NAME = session_data; Note that SESSION_NAME has to be identical with "name" attribute's value you have declared in step 5
Delete session:
res.clearCookie("SESSION_NAME"); req.session.destroy();
Hope this helps somebody, who encountered this issue.

JWT Login - No authorization token was found in middleware

I followed a tutorial to add login and registration to my Node.js app using JWT token and I'm having a hard time logging in and redirecting to my 'logged in' admin page. User registration works great, but the login portion I can't figure out.
This is the tutorial I was following:
https://medium.freecodecamp.org/learn-how-to-handle-authentication-with-node-using-passport-js-4a56ed18e81e
My code for login looks like this:
router.post('/login', auth.optional, (req, res, next) => {
console.log(req.body);
var user = {
email: req.body.email,
password: req.body.password
}
if (!user.email) {
return res.status(422).json({
errors: {
email: 'is required',
},
});
}
if (!user.password) {
return res.status(422).json({
errors: {
password: 'is required',
},
});
}
return passport.authenticate('local', { session: false }, (err, passportUser, info) => {
if (err) {
return next(err);
}
if (passportUser) {
const user = passportUser;
user.token = passportUser.generateJWT();
console.log("TOKEN: " + user.token);
res.setHeader('Authorization', 'Token ' + user.token);
return res.json({ user: user.toAuthJSON() });
}
return res.status(400).json({
errors: {
message: info,
},
});
})(req, res, next);
});
My '/admin' "logged in" route looks like this:
router.get("/admin", auth.required, function(req, res) {
res.render('admin', {
user : req.user // get the user out of session and pass to template
});
});
I'm not sure how I can redirect to my '/admin' route while also passing the token because currently I am seeing the following error after logging in. Makes sense since I am not passing the token to the '/admin' route...but how do I do that? :)
UnauthorizedError: No authorization token was found at middleware
Thanks in advance for the help!
EDIT:
Still can't figure this out and don't really understand how this flow is supposed to work...where do the headers need to be set to the token and how do I redirect to my admin page once the login is successful.
Here is my middleware code if this helps:
const getTokenFromHeaders = (req) => {
console.log("REQ: " + JSON.stringify(req.headers));
const { headers: { authorization } } = req;
if(authorization && authorization.split(' ')[0] === 'Token') {
return authorization.split(' ')[1];
}
return null;
};
const auth = {
required: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders,
}),
optional: jwt({
secret: 'secret',
userProperty: 'payload',
getToken: getTokenFromHeaders,
credentialsRequired: false,
}),
};
Your code does not have a problem. You seem to be confused with the login flow from server to client (Frontend/Web).
Let's first have a look the RESTFUL way of doing it. The article also refers to the same flow.
The RESTFUL API flow looks like this:
User requests for login:
POST: /api/v1/auth/login with username and password in request body.
If successful, user is returned with basic inforamtion and token.
If not, user is returned a 401 (Unauthorized) status code.
The login flow ends here.
The token provided earlier to the user is used to make subsequent calls to the backend, which a user can use to perform different operations on the sustem. In essence, it is the client which requests server for subsequent actions with the token provided in the login request.
So for your case, user after receiving the token should make a request for retrieving admin information from the backend.
But, I am assuming you are rendering views from your server-side and you want to render the admin view once the user is successfully logged in, and that's pretty straight forward.
Instead of your res.json() after successful login. You need to use res.render().
res.render('admin', {
user: user.toAuthJSON() // assuming your user contains the token already
})
Edit:
Since res.render() does not change the url in the browser. For that, you need to use res.redirect(). But the problem is, you can not send context in res.redirect().
To achieve that, you will need to pass in the user token as query paramter. See here.
TL;DR
// assuming you are using Node v7+
const querystring = require('querystring');
const query = querystring.stringify({
token: user.token,
});
const adminRoute = '/admin?' + query;
res.redirect(adminRoute)
And in your admin route, you need to slightly modify the code.
Verify the token belongs to a real user and get user information out of the token.
Render the admin template with user information retrieved from step 1.
router.get("/admin", function(req, res) {
// verify the token
const token = req.query.token;
const user = null;
jwt.verify(token, 'secret', function (err, decoded) {
if (err) {
res.status(401).send('Unauthorized user')
}
// decoded contains user
user = decoded.user
});
res.render('admin', {
user : user
});
});
I'm somewhat new to this as well, but I've got it working as follows.
In your server.js file:
const passport = require("passport");
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
app.use(passport.initialize());
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = Keys.secretOrKey;
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
// somefunction looks up the id in jwt payload and
// supplies passport the authenticated user via the "Done" function
somefunction.user(jwt_payload.id)
.then(user => {
if (user) {
return done(null, user);
}
return done(null, false);
});
})
);
In your API definitions
const jwt = require("jsonwebtoken");
router.post("/login", (req, res) => {
const { userInfo } = req.body;
// userInfo has username and password in it
// anotherFuction validates the user id and password combo
anotherFunction(userInfo.id, userInfo.password)
.then(isAuthenticated => {
if (isAuthenticated) {
const payload = {
id: user.sAMAccountName,
firstname: user.givenName,
lastname: user.sn
};
// Sign Token with the payload
jwt.sign(
payload,
Keys.secretOrKey,
{ expiresIn: 3600 },
(err, token) => {
res.json({
success: true,
token: "Bearer " + token
});
}
);
} else {
// don't mind the statuses ^_^'
return res.status(401).json({ error: "Login failed." });
}
})
.catch(err => {
return res.status(400).json(err);
});
});
After calling the API you want to set the auth token. The following lets you delete the token if nothing is passed in, effectively "Logging out".
const setAuthToken = token => {
if (token) {
// Apply to every request
axios.defaults.headers.common["Authorization"] = token;
} else {
// Delete Auth Header
delete axios.defaults.headers.common["Authorization"];
}
};
If you're trying to use it in the front end, you need to use jwt_decode to pull the values from the token and set it however you deem necessary. If using redux to store login data it should look something like this. As I feel that the discussion of using localstorage for jwtToken is outside of the scope of this, just know would need to check for the token.
if (localStorage.jwtToken) {
setAuthToken(localStorage.jwtToken);
const decoded = jwt_decode(localStorage.jwtToken);
store.dispatch({
type: USER_LOGIN,
payload: decoded
});
}
Hope this helped.
From one beginner in JWT to another. Good luck.

Express Session property differentiates between browser and Postman

UPDATE
I think it's worth mentioning I am running Angular CLI which runs on port 4200 and my server is running on port 8080. Could this be a problem? It's the only thing I can think of at the moment
When I make a call to my route '/auth/login' I set a loggedIn property on the session object. To check a user is authenticated, a request is made to '/auth/checktoken'. In here, I check for the presence of the loggedIn property on the req.session object. When I do these requests within Postman everything works perfectly fine, but when using the browser my session.loggedIn property is undefined. I will paste the relevant code below. Thanks in advance for any help
Server Side
router.get('/checktoken', (req, res) => {
if(!req.session.loggedIn) {
return res.status(401).send({
userTitle: 'Not authorised',
userMessage: 'You are not authorised to view this'
})
}
return res.status(200).send()
})
Client Side
#Injectable()
export class CheckAuthenticationService implements CanActivate {
constructor(
private router: Router,
private http: HttpClient) { }
canActivate() {
this.http.get('http://localhost:8080/auth/checktoken', { responseType: 'text' })
.toPromise()
.then(() => {
this.router.navigate(['admin']);
})
.catch( () => {
this.router.navigate(['login']);
});
return true;
}
}
Snippet of login code that sets the loggedIn property
if (user) {
user.comparePassword(password, (err, isMatch) => {
if (isMatch && isMatch) {
req.session.loggedIn = user;
res.status(200).send()
} else {
res.status(404).send({
userTitle: 'Wrong password',
userMessage: 'Please make sure your password is correct'
});
}
});
}
Session Store setup
app.use(session({
name: 'jack-thomson',
secret: SECRET_KEY,
saveUninitialized: false,
resave: true,
store: new MongoStore({
mongooseConnection: mongoose.connection
})
}))
This all works in Postman but when hitting these endpoints on the client, .loggedIn is undefined, always
I had the same problem before. I think it's about cors credential. I use Axios on React to POST data login to my Express backend application. I need to add these lines:
import axios from 'axios';
axios.defaults.withCredentials = true;
Then on my Express project, I add cors:
var cors = require('cors');
app.use(cors({
credentials: true,
origin: 'http://localhost:3000' // it's my React host
})
);
Finally I can call my login function as usual, for instance:
signup(){
var url = 'http://localhost:3210/'
axios.post(url, {
email: this.refs.email.value,
username: this.refs.username.value,
password: this.refs.password.value,
passwordConf: this.refs.passwordConf.value
})
.then((x)=>{
console.log(x);
if(x.data.username){
this.setState({statusSignup: `Welcome ${x.data.username}`});
} else {
this.setState({statusSignup: x.data});
}
})
.catch((error)=>{console.log(error)})
}
login(){
var url = 'http://localhost:3210/';
var data = {
logemail: this.refs.logemail.value,
logpassword: this.refs.logpassword.value,
};
axios.post(url, data)
.then((x)=>{
console.log(x);
if(x.data.username){
this.setState({statusLogin: `Welcome ${x.data.username}`});
} else {
this.setState({statusLogin: x.data});
}
})
.catch((error)=>{console.log(error)})
}
And it works! Hope this solve your problem.
Are you using CORS?
I had the same problem, and i solved it by putting { withCredentials: true } as optional arguments in every request.
I mean whenever you send a http/https request in your service, put this as last argument, and you are good to go.
You can read this and this Stackoverflow question for more information on the topic.
I have finally figured out what is going on. My Angular CLI was running on 4200 and my server was running on a separate port. I have gotten over the issue with serving my application with express so it is all one one route. This has solved the issue for me. If anyone comes by this I hope this information comes in handy to you!

Can't delete cookie in express

Pretty simple. I set a cookie like so in my /user/login route:
if (rememberMe) {
console.log('Login will remembered.');
res.cookie('user', userObj, { signed: true, httpOnly: true, path: '/' });
}
else {
console.log('Login will NOT be remembered.');
}
I've already set my secret for cookie-parser:
app.use(cookieParser('shhh!'));
Pretty basic stuff. Everything is working great insofar as I'm able to retrieve whatever I stored in the cookie:
app.use(function (req, res, next) {
if (req.signedCookies.user) {
console.log('Cookie exists!');
req.session.user = req.signedCookies.user;
}
else {
console.log('No cookie found.');
}
next();
});
This middleware is called before anything else, so for the sake of the argument "Cookie exists!" is always logged in my console if the cookie is valid.
The problem is when I try to delete the cookie. I've tried res.clearCookie('user'), res.cookie('user', '', { expires: new Date() }), and I've tried passing in the same flags (that I pass to res.cookie() in /user/login). I've attempted to use combinations of these methods, but nothing has worked.
Currently, the only way I am able to clear the cookie (and not receive the "Cookie exists!" log message) is by clearing my browser history. Here is what my logout route looks like:
route.get('/user/logout', function (req, res, next) {
res.clearCookie('user');
req.session.destroy();
util.response.ok(res, 'Successfully logged out.');
});
It seems as though I can't even modify the cookie value; I put
res.cookie('user', {}, { signed: true, httpOnly: true, path: '/' })
in my logout route, but the cookie value remains unchanged.
I realized after a long and annoying time that my front end was not sending the cookie to the end point were I was trying to clear the cookie...
On the server:
function logout(req, res) {
res.clearCookie('mlcl');
return res.sendStatus(200);
}
And on the front end,
fetch('/logout', { method: 'POST', credentials: 'same-origin' })
adding the "credentials: 'same-origin'" is what made the clearCookie work for me. If the cookie is not being sent, it has nothing to clear.
I hope this helps. I wish I had found this earlier...
Even though it's not gonna help the author of this question, i hope this might help someone.
I run into the same problem that i could not delete cookies in my React app that was using Express api. I used axios, and after a couple of hours i was finally able to fix it.
await axios.post('http://localhost:4000/api/logout', { } , { withCredentials: true })
{ withCredentials: true } is what made it work for me.
This is my Express code:
const logOutUser = (req, res) => {
res.clearCookie('username')
res.clearCookie('logedIn')
res.status(200).json('User Logged out')
}
Judging by (an extensive) search and a random thought that popped into my head, the answer is to use
res.clearCookie('<token_name>',{path:'/',domain:'<your domain name which is set in the cookie>'});
i.e.
res.clearCookie('_random_cookie_name',{path:'/',domain:'.awesomedomain.co'});
Note the . which is specified in the cookie, because we use it for subdomains (you can use it for subdomains without the dot too, but it's simply safer to use one).
TLDR; You have to provide a route and domain: in the backend, so that the request is made to the same endpoint in the frontend.
Make sure you are sending your credentials to be cleared
Even though it's only a /logout endpoint, you still need to send credentials.
// FRONT END
let logOut = () => {
fetch('logout', {
method: 'get',
credentials: 'include', // <--- YOU NEED THIS LINE
redirect: "follow"
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
});
}
// BACK END
app.get('/logout', (req, res) => {
res.clearCookie('token');
return res.status(200).redirect('/login');
});
Had a nightmare getting this to work as well this works for me hopefully helps someone.
Express router
router.post('/logout', (req, res) => {
res.clearCookie('github-token', {
domain: 'www.example.com',
path: '/'
});
return res.status(200).json({
status: 'success',
message: 'Logged out...'
});
});
React frontend handle logout.
const handleLogout = async () => {
const logout = await fetch('/logout', {
method: 'POST',
credentials: 'include',
});
if (logout.status === 200) {
localStorage.clear();
alert('Logged out');
} else {
alert('Error logging out');
}
};
I am setting the cookie in my auth call like this.
res.cookie('github-token', token, {
httpOnly: true,
domain: 'www.example.com',
secure: true
});
Important you need to add the path and domain in the clearCookie method.
Path needs to be correct. In my case it was a typo in path
Nov 2022 (Chrome) - What worked for me
Frontend:
const logOut = async () =>{
await axios.post(LOGOUT_URL, {}, {withCredentials: true}) // <-- POST METHOD, WITH CREDENTIALS IN BODY
}
Backend:
res.clearCookie('jwt') // <- NO EXTRA OPTIONS NEEDED, EVEN THOUGH HTTPONLY WAS SET
return res.sendStatus(204)
I am new to this but adding a return (return res.sendStatus(204);)to Backend function is what deleted the cookie for me, hope it helps.
without return it does not delete the cookie but only logs "session over"
app.post("/logout", (req, res) => {
if (req.session.user && req.cookies.user_sid) {
res.clearCookie("user_sid");
console.log("session over");
return res.sendStatus(204);
} else {
console.log("error");
}
});

Categories

Resources