I am working on user authentication for a website built using the MERN stack and I have decided to use JWT tokens stored as HttpOnly cookies. The cookie was sent in a "Set-Cookie" field in response header when I used Postman to make the request but not in the Safari Web Inspector as shown in the image below. There are no cookies found in the storage tab either.
I have simplified my React login form to a button that submits the username and password of the user for the sake of debugging
import React from "react";
const sendRequest = async (event) => {
event.preventDefault();
let response;
try {
response = await fetch("http://localhost:5000/api/user/login", {
method: "POST",
body: { username: "Joshua", password: "qwerty" },
mode: "cors",
// include cookies/ authorization headers
credentials: "include",
});
} catch (err) {
console.log(err);
}
if (response) {
const responseData = await response.json();
console.log(responseData);
}
};
const test = () => {
return (
<div>
<input type="button" onClick={sendRequest} value="send" />
</div>
);
};
export default test;
I am using express on the backend and this is my index.js where all incoming requests are first received
const app = express();
app.use(bodyParser.json());
app.use("/images", express.static("images"));
app.use((req, res, next) => {
res.set({
"Access-Control-Allow-Origin": req.headers.origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type, *",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE",
});
next();
});
app.use(cookieParser());
// api requests for user info/ login/signup
app.use("/api/user", userRoutes);
This is the middleware that the login request is eventually directed to
const login = async (req, res, next) => {
const { username, password } = req.body;
let existingUser;
let validCredentials;
let userID;
let accessToken;
try {
existingUser = await User.findOne({ username });
} catch (err) {
return next(new DatabaseError(err.message));
}
// if user cannot be found -> username is wrong
if (!existingUser) {
validCredentials = false;
} else {
let isValidPassword = false;
try {
isValidPassword = await bcrypt.compare(password, existingUser.password);
} catch (err) {
return next(new DatabaseError(err.message));
}
// if password is wrong
if (!isValidPassword) {
validCredentials = false;
} else {
try {
await existingUser.save();
} catch (err) {
return next(new DatabaseError(err.message));
}
userID = existingUser.id;
validCredentials = true;
accessToken = jwt.sign({ userID }, SECRET_JWT_HASH);
res.cookie("access_token", accessToken, {
maxAge: 3600,
httpOnly: true,
});
}
}
res.json({ validCredentials });
};
Extra information
In the login middleware, a validCredentials boolean is set and returned to the client. I was able to retrieve this value on the front end hence I do not think it is a CORS error. Furthermore, no errors were thrown and all other API requests on my web page that do not involve cookies work fine as well.
Another interesting thing is that despite using the same data (A JS object containing {username:"Joshua", password:"qwerty"}) for both Postman and the React code, validCredentials evaluates to true in Postman and false in the Web Inspector. It is an existing document in my database and I would expect the value returned to be true, which was the case before I added cookies
May I know what I have done wrong or do you have any suggestions on how I can resolve this issue? I am a beginner at web-development
EDIT
With dave's answer I can receive the "Set-Cookie" header on the frontend. However it does not appear in the Storage tab in the web inspector for some reason.
This is the response header
This is the Storage tab where cookies from the site usually appears
If you're trying to send the request as json, you need to set the content type header, and JSON.stringify the object:
response = await fetch("http://localhost:5000/api/user/login", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: "Joshua", password: "qwerty" }),
mode: "cors",
// include cookies/ authorization headers
credentials: "include",
});
Right now you're probably getting the equivalent of
existingUser = User.findOne({ username: undefined})
and so when you do:
if (!existingUser) {
validCredentials = false;
} else { /* ... */ }
you get the validCredentials = false block, and the cookie is set in the other block.
You can not see it because you have made it httpOnly cookie.
Related
I am trying to build a project in which I will fetch the user's step count by using the google fit Rest API. For this, I have created a project on google's developer console and specified a redirection url there. Have a look to the code snippet below :
exports.getUrl = async (req, res, next) => {
try {
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_FIT_CLIENT_ID,
process.env.GOOGLE_FIT_CLIENT_SECRET,
process.env.GOOGLE_FIT_REDIRECTION_URL
);
console.log("oauth2Client", oauth2Client)
// generate a url that asks permissions for fitness activity scopes
const scopes = ["https://www.googleapis.com/auth/fitness.activity.read profile email openid"];
const url = oauth2Client.generateAuthUrl({
access_type: "offline",
scope: scopes,
include_granted_scopes: true,
state: JSON.stringify({
// callbackurl: req.body.callbackurl,
})
});
console.log("url", url);
res.redirect(302, url);
} catch (err) {
console.log("err", err)
next(err);
}
}
exports.getSteps = async (req, res, next) => {
try {
const queryUrl = new urlParse(req.url);
const code = queryParse.parse(queryUrl.query).code;
const oauth2Client = new google.auth.OAuth2(
process.env.GOOGLE_FIT_CLIENT_ID,
process.env.GOOGLE_FIT_CLIENT_SECRET,
process.env.GOOGLE_FIT_REDIRECTION_URL
);
const token = await oauth2Client.getToken(code);
oauth2Client.setCredentials(token);
const result = await axios({
proxy: {
protocol: 'http',
host: 'proxy-url',
port: port
},
method: "POST",
headers: {
authorization: "Bearer " + token.tokens.access_token
},
"Content-Type": "application/json",
url: "https://www.googleapis.com/fitness/v1/users/me/dataset:aggregate",
data: {
"aggregateBy": [{
"dataTypeName": "com.google.step_count.delta",
"dataSourceId": "derived:com.google.step_count.delta:com.google.android.gms:estimated_steps"
}],
"bucketByTime": { "durationMillis": 86400000 }, // This is 24 hours
"startTimeMillis": startTime, // This startTime and endTime I am getting from DB
"endTimeMillis": endTime
}
});
if (result) {
const response = [];
let stepArray = result?.data?.bucket;
for (const dataSet of stepArray) {
for (const points of dataSet.dataset) {
for (const steps of points.point) {
response.push(steps?.value[0]?.intVal);
}
}
}
res.status(200).send(response);
} else {
throw new Error('Data fetching failed!');
}
} catch (err) {
next(err);
}
}
The steps url is what I have mentioned as a redirection url on the google's developer console. I have used proxy because the urls which are getting called are not whitelisted on the server on which I am deploying the code.
Now, everything worked perfectly fine in localhost but on server, I am getting below error :
Access to XMLHttpRequest at 'https://accounts.google.com/o/oauth2/v2/auth?access_type=offline' (redirected from 'https://someexample.com?id=123') from origin 'https://someexample.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Also, one thing to note here is that the above error not coming when I am trying to get the data from chrome with --disable-web-security flag.
I have mentioned the uri through which this request is originating ( ex- https://someexample.com) in authorised JavaScript origins on the Google's developer console too.
Please let me know if I am doing something wrong. Any response would be highly appreciated.
I'm using zendesk OAuth for authorization. I'm using the MERN stack and the current implementation works like this,
User clicks login and redirected to zendesk
once the user signs I get redirected back to /callback path
Where I sent another request to get an auth token
After I get the token I redirect the user to frontend as ?token=XXXX attached to the URL
Is this the correct way? How should I proceed with the token should I keep it in session storage? It's not a good idea to expose the token?
export const authCallback = (req: Request, res: Response): void => {
const body = {
grant_type: 'authorization_code',
code: req.query.code,
client_id: process.env.ZENDESK_CLIENT_ID,
client_secret: process.env.ZENDESK_SECRET,
}
axios
.post(`https://${process.env.SUBDOMAIN}.zendesk.com/oauth/tokens`, body, {
headers: {
'Content-Type': 'application/json',
}
})
.then((response) => {
const token = response.data.access_token
return res.redirect(`${process.env.ORIGIN}?token=${token}`)
})
.catch((err) => {
return res.status(400).send({ message: err.message })
})
}
Either use express-session and store the token on the server in req.session.token:
(response) => {
req.session.token = response.data.access_token;
req.session.save(function() {
res.redirect(`${process.env.ORIGIN}`)
});
}
Or send the token in a session cookie directly:
(response) => {
res.cookie("token", response.data.access_token, {
httpOnly: true,
secure: true,
sameSite: "None"
});
res.redirect(`${process.env.ORIGIN}`)
}
I have an api in express js that stores token in cookie on the client-side (react). The cookie is generated only when the user logins into the site. For example, when I test the login api with the postman, the cookie is generated as expected like this:
But when I log in with react.js then no cookie is found in the browser. Looks like the cookie was not passed to the front end as the screenshot demonstrates below:
As we got an alert message this means express api is working perfectly without any error!!
Here is my index.js file on express js that includes cookie-parser middleware as well
require("dotenv").config();
const port = process.env.PORT || 5050;
const express = require("express");
const app = express();
const cors = require("cors");
const authRouter = require("./routes/auth");
var cookieParser = require('cookie-parser')
connect_db();
app.use(express.json());
app.use(cookieParser())
app.use(cors());
app.use("/" , authRouter);
app.listen(port , () => {
console.log("Server is running!!");
})
Code for setting up the cookie from express api only controller
const User = require("../models/user");
const jwt = require("jsonwebtoken");
const bcrypt = require('bcrypt')
const login = async (req, res) => {
const { email, password } = req.body;
try {
const checkDetails = await User.findOne({ email });
if (checkDetails) {
const { password: hashedPassword, token, username } = checkDetails;
bcrypt.compare(password, hashedPassword, function (err, matched) {
if (matched) {
res.cookie("token", token, { expires: new Date(Date.now() + (5 * 60000)) , httpOnly: true }).json({ "message": "You logged in sucessfully!" });
} else {
res.status(500).json({ "message": "Wrong password" });
}
});
} else {
res.status(500).json({ "message": "Wrong email" });
}
} catch (error) {
console.log(error.message);
}
}
Here is the react.js code that I am using to fetch data from api without using a proxy in package.json file
if (errors.length === 0) {
const isLogin = await fetch("http://localhost:5000/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
headers: {
"Content-Type": "application/json"
}
});
const res = await isLogin.json();
if(res) alert(res.message);
}
I want to get to know what is the reason behind this "getting cookie in postman but not in the browser". Do I need to use any react package?
The network tab screenshot might help you.
If I see in the network tab I get the same cookie, set among the other headers
To my understanding, fetch doesn't send requests with the cookies your browser has stored for that domain, and similarly, it doesn't store any cookies it receives in the response. This seems to be the expected behaviour of fetch.
To override this, try setting the credentials option when making the request, like so:
fetch(url, {
// ...
credentials: 'include'
})
or, alternatively:
fetch(url, {
// ...
credentials: 'same-origin'
})
You can read more about the differences between the two here.
I got my error resolved with two changings in my code
In front end just added credentials: 'include'
fetch(url, {
method : "POST"
body : body,
headers : headers,
credentials: 'include'
})
And in back end just replaced app.use(cors()); to
app.use(cors({ origin: 'http://localhost:3000', credentials: true, exposedHeaders: ['Set-Cookie', 'Date', 'ETag'] }))
That's it got resolved, Now I have cookies stored in my browser!!! Great. Thanks to this article:
https://www.anycodings.com/2022/01/react-app-express-server-set-cookie-not.html
during development i also faced same things, let me help you that how i solve it,
Firstly you use proxy in your react package.json, below private one:-
"private": true,
"proxy":"http://127.0.0.1:5000",
mention the same port on which your node server is running
Like:-
app.listen(5000,'127.0.0.1',()=>{
console.log('Server is Running');
});
above both must be on same , now react will run on port 3000 as usual but now we will create proxy to react So, react and node ports get connected on same with the help of proxy indirectly.
Now, when you will make GET or POST request from react then don't provide full URL, only provide the path on which you wants to get hit in backend and get response,
Example:-
React side on sending request, follow like this:-
const submitHandler=()=>{
axios.post('/api/loginuser',
{mobile:inputField.mobile,password:inputField.password})
.then((res)=>{
console.log(res);
})
.catch((err)=>{
console.log(err);
})
}
Node side where it will hit:-
app.post('/api/loginuser', async(req,res)=>{
//Your Code Stuff Here
res.send()
}
on both side same link should hit, it is very important
it will 100%.
don't forget to mention
on node main main where server is listening
EDIT - I no longer think it is a development environment issue as the same thing persists in production.
First off, appreciate anytime spent on this - I know that this is a commonly asked question, but I've perused so many questions and tried applying so many of the solutions to no avail.
To summarize, I currently have a React Front-end that is sending request with the Fetch API to my Express server. I want to use express-sessions to save my logged in user information for the session to use and authenticate as the user uses the application.
I'm having this weird issue where every time a new request is sent, the cookie in my browser in the application tab gets set to something completely new. I might be understanding this wrong, but I thought that that was supposed to remain consistent throughout the session, but it doesn't seem to be the case. This is making it so that every time I call a new request, req.session is completely reset with nothing.
Here is some code from my application:
Express- server.js [my setup for express-session and cors]
app.use(cors({
credentials: true
}))
const session = require('express-session')
// Middleware for creating sessions and session cookies.
// A session is created on every request
app.use(session({
secret: 'tis a secret mate',
cookie: {
expires: 3000, // expires in 15 mins
httpOnly: true,
secure: false,
},
// Session saving options
saveUnintialized: false, // don't save the initial session if the session object is unmodified (i.e the user did not log in)
resave: true, // don't resave a session that hasn't been modified
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI_SESSIONS
})
}))
app.use(function (req, res, next) {
console.log('SESSION LOGGING MIDDLEWARE')
console.log(req.session);
next()
});
app.use((req, res, next) => {
res.header('Access-control-Allow-Origin', 'http://localhost:3000');
res.header(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
);
res.header('Access-Control-Allow-Credentials', true);
next();
});
EDIT - Login Route
app.post('/api/users/login', async (req, res) => {
// pull out the body
const body = req.body
// pull out the username and password
const username = body.username
const password = body.password
User.findByUsernamePassword(username, password)
.then(user => {
// if we find the user, the users information to the session to retain all that information
// will create a function to ensuree that the session exists to make sure that we are logged in
req.session.user = user
req.session.username = user.username
req.session.save()
console.log(req.session)
res.send({ user: req.session.user })
})
.catch(error => {
res.status(400).send()
});
})
Front-end Fetch Calls
In the code below, I pasted my login and check-session calls to their respective endpoints. Essentially the check session call just returns what is in the session, and the login call is supposed to login the user and add the user to req.session.user
After logging in, I try to run check-session to see if the user is in the session, but it is not there every time. I also notice that the cookie in my applications tab for chrome changes to something else.
export const login = (username, password, setUser) => {
const obj = {
username: username,
password: password
}
const request = new Request(`${API_HOST}/api/users/login`, {
method: "POST",
credentials: "include",
body: JSON.stringify(obj),
headers: {
"Content-Type": "application/json"
}
})
// send the request
const promise = fetch(request)
.then(res => {
if (res.status === 200) {
return res.json();
}
})
.then(json => {
// if the user exists, setUser to user
console.log("USER PRE:", json.user)
if (json.user !== undefined) {
console.log("USER: ", json.user)
setUser(json.user)
return json.user
}
})
.catch(error => {
console.log(error);
})
return promise
}
export const checkSession = () => {
const url = `${API_HOST}/api/users/check-session`
fetch(url, {
credentials: 'include'
})
.then(res => {
if (res.status === 200) {
console.log('200')
console.log(res.json())
} else {
console.log(res)
}
})
// .then(json => {
// if (json) {
// setUser(json)
// }
// })
.catch(error => {
console.log(error);
});
}
Things I've Tried
I've tried changing things to how my cookie is defined in server.js (secure: false | httpOnly: true)
I've tried adding credentials to my fetch calls and accepting them in cors in server.js
I changed my chrome to disable-web-security
I added cors access control stuff to the response header as seen above
Additional notes that might be helpful
I'm using React with React Router, not sure if that would affect stuff
Thank you for your time, I know this is a super long question. Please let me know if there is anything else I could provide to help clarify if you want to help!
If you're running the backend and frontend on different hosts, there could be a CORS problem. Also you said that you checked the cookie in the application tab, but if you haven't tried looking at the request and response cookie headers in the network tab, you should check those out to further diagnose the problem. The cookie should be set on the authentication request, and it should be included in the requests following that.
So the first time running this code, the browser set the cookie. But then when I cleared the cookie from the browser and tried to run the code again, the browser isn't setting the cookie any more. I tried multiple browsers and it doesn't work. The fetch request is successful and i can print the cookie value in the console, but the browser wont set the cookie. This is a very annoying problem. I tried using the credential: 'include' also but it didnt work. Getting a CORS error.
I'm pretty new to web development so my knowledge is not very deep.
This is the code on my front end
let reqObj = {
//check email to see if it is a valid format in the login.html user email input
//name: name.value,
email: email.value,
password: password.value
}
const response = await fetch(api_url + "api/user/login", {
method: 'POST',
body: JSON.stringify(reqObj),
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
});
const jwt = await response.text();
console.log(jwt)
this is the express server code
router.post('/login', async (req, res) => {
//validate data
const { error } = loginValidation(req.body)
if (error) return res.status(400).send(error.details[0].message);
//checking if email is in database
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(400).send('Email or password is incorrect')
//Password is correct
const validPass = await bcrypt.compare(req.body.password, user.password)
if (!validPass) return res.status(400).send('Invalid password')
// Create and assign a token
const token = jsonWebToken.sign({ _id: user.id }, process.env.TOKEN_SECRET);
//res.header('auth-token', token).send(token);
res.cookie('auth_token', token, {
maxAge: 3600,
httpOnly: true
}).send(token)
});
module.exports = router;
I believe that in order to use credentials: 'include' you have to add the Access-Control-Allow-Credentials header on your response.