I am building a login system using express for node.js and react.js. In my back-end when a user logs in, it creates a cookie. When I go to Network > Login I can see this:
Set-Cookie:
user_id=s%3A1.E%2FWVGXrIgyXaM4crLOoxO%2Fur0tdjeN6ldABcYOgpOPk; Path=/; HttpOnly; Secure
But when I go to Application > Cookies > http://localhost:3000, there is nothing there. I believe that is because I am not allowing credentials to go through correctly when I do a post request from the client side. How do I go about this? Please, let me know if I can improve my question in any way.
//Login back-end
router.post('/login', (req, res, next) => {
if(validUser(req.body)) {
User
.getOneByEmail(req.body.email)
.then(user => {
if(user) {
bcrypt
.compare(req.body.password_digest, user.password_digest)
.then((result) => {
if(result) {
const isSecure = process.env.NODE_ENV != 'development';
res.cookie('user_id', user.id, {
httpOnly: true,
secure: isSecure,
signed: true
})
res.json({
message: 'Logged in'
});
} else {
next(new Error('Invalid Login'))
}
});
} else {
next(new Error('Invalid Login'))
}
});
} else {
next(new Error('Invalid Login'))
}
});
//Allow CORS index.js
app.use(
cors({
origin: "http://localhost:3000",
credentials: true
})
);
//Login client side (React.js)
loginUser(e, loginEmail, password) {
e.preventDefault();
let email = loginEmail;
let password_digest = password;
let body = JSON.stringify({ email, password_digest });
fetch("http://localhost:5656/api/login", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
credentials: "include",
body
})
.then(response => response.json())
.then(user => {
console.log(user);
});
}
You should be secure of set "credentials" in the server and in app.
Try to set on you index.js or app.js server side this:
app.use(function(req, res, next) {
res.header('Content-Type', 'application/json;charset=UTF-8')
res.header('Access-Control-Allow-Credentials', true)
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
)
next()
})
and in you client site add options like this:
let axiosConfig = {
withCredentials: true,
}
export async function loginUser(data) {
try {
const res = await axios.post(
`${URL}:${PORT}/${API}/signin`,
data,
axiosConfig
)
return res
} catch (error) {
console.log(error)
}
}
Edit
To set "credentials" in server we need this line:
res.header('Access-Control-Allow-Credentials', true)
This would let you handle credentials includes in headers.
You also have to tell to axios to set credentials in headers with:
withCredentials: true
Do not forget to adjust cors middleware.
Your node.js express code
const express = require("express");
const cors = require('cors')
const app = express();
app.use(cors(
{
origin: 'http://localhost:3000',
optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204
}
));
app.use(function(req, res, next) {
res.header('Content-Type', 'application/json;charset=UTF-8')
res.header('Access-Control-Allow-Credentials', true)
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
)
next()
})
app.get("/auth", function(req, res){
res.cookie('token', 'someauthtoken')
res.json({id: 2});
});
app.listen(3030);
Your front-end code
import React, { useEffect } from 'react';
import axios from 'axios';
async function loginUser() {
try {
const res = await axios.get(
'http://localhost:3030/auth',
{
withCredentials: true,
}
)
return res
} catch (error) {
console.log(error)
}
}
function App() {
useEffect(() => {
loginUser();
}, [])
return (
<div>
</div>
);
}
export default App;
It is because you set httpOnly: true.
This will block the visibility to client side, like reading from javaScript document.cookie().
You can solve this by turn it off.
If you can't see your cookie in the browser, I think it is because you're setting hhtpOnly to true in the cookie's options.
cookie.httpOnly
Specifies the boolean value for the HttpOnly Set-Cookie attribute. When truthy, the HttpOnly attribute is set, otherwise it is not. By default, the HttpOnly attribute is set.
Note: be careful when setting this to true, as compliant clients will not allow client-side JavaScript to see the cookie in document.cookie
res.cookie('user_id', user.id, {
httpOnly: false, // try this
secure: isSecure,
signed: true
})
You need to configure cors in your backend server first.
First, install cors using npm i cors then in your express server add this line of code:
app.use(cors({
origin: "YOUR FRONTEND SITE URL HERE",
credentials: true,
}));
Then, in your frontend app where you are sending GET/POST requests to your backend, make sure to add in your request
If you've used fetch:
const res = await fetch('BACKEND SERVER URL', {
credentials: "include",
// other objects
});
If axios is used:
const res = await axios.post('BACKEND SERVER URL',
{ withCredentials: true },
// other objects,
);
This will solve the problem of storing cookies in frontend sent from backend.
Related
I'm trying to integrate passport-google-oauth20 in my MERN application and although everything works fine in development on my local host but in production, it keeps throwing this error;
No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
I have searched and gone through multiple stack overflow posts and tried some of the answers and suggestions but nothing seems to work. This my index.js and the codes commented out are some of the solutions and CORS settings I've tried. I also tried putting the URL directly instead of an environment variable, but that didn't work either.
const express = require("express");
const cookieSession = require("cookie-session");
const passport = require("passport");
const cors = require("cors");
const helmet = require("helmet");
const connectDB = require("./config/db");
const authRoute = require("./routes/auth.route");
const userRoute = require("./routes/user.route");
const adminRoute = require("./routes/admin.route");
const transactionRoute = require("./routes/transaction.route");
//Passport setup
require("./passport");
const path = require("path");
// require("dotenv").config();
const app = express();
require("dotenv").config({
path: "./config/config.env",
});
//Connect to Database
connectDB();
//Use bodyParser
app.use(express.json());
app.use(
helmet({
contentSecurityPolicy: false,
frameguard: true,
})
);
app.use(
cookieSession({
name: "session",
keys: ["ccurves"],
maxAge: 24 * 60 * 60 * 100,
})
);
app.use(passport.initialize());
app.use(passport.session());
//Config for only development
// if (process.env.NODE_ENV === "development") {
// app.use(
// cors({
// origin: process.env.CLIENT_URL,
// methods: "GET,POST,PUT,DELETE",
// credentials: true,
// })
// );
// }
// const corsOptions = {
// origin: [`${process.env.CLIENT_URL}`],
// methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
// allowedHeaders: [
// "Access-Control-Allow-Headers",
// "Origin",
// "X-Requested-With",
// "Content-Type",
// "Accept",
// "Authorization",
// "token",
// "Access-Control-Request-Method",
// "Access-Control-Request-Headers",
// "Access-Control-Allow-Credentials",
// ],
// credentials: true,
// preflightContinue: false,
// optionsSuccessStatus: 204,
// };
// app.use(cors(corsOptions));
app.use(
cors({
origin: process.env.CLIENT_URL,
methods: "GET,POST,PUT,DELETE",
credentials: true,
})
);
// app.use((req, res, next) => {
// res.header("Access-Control-Allow-Origin", `${process.env.CLIENT_URL}`);
// res.header("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
// res.header("Access-Control-Allow-Credentials", true);
// res.header(
// "Access-Control-Allow-Headers",
// "Access-Control-Allow-Headers, Origin, X-Requested-With, Content-Type, Accept, Authorization, token, Access-Control-Request-Method, Access-Control-Request-Headers, Access-Control-Allow-Credentials, Access-Control-Allow-Origin"
// );
// next();
// });
app.use("/api/auth", authRoute);
app.use("/api/user", userRoute);
app.use("/api/admin", adminRoute);
app.use("/api/transaction", transactionRoute);
const port = process.env.PORT;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
And in my react frontend I'm fetching the request from the API like this:
const getUser = () => {
fetch(`${process.env.REACT_APP_API_URL}/auth/login/success`, {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
})
.then((response) => {
if (response.status === 200) return response.json();
throw new Error("authentication has been failed!");
})
.then((resObject) => {
authenticate(resObject, () => {
isAuth && navigate("/");
});
})
.catch((err) => {
console.log(err);
});
};
I also tried using axios to send the request,
axios
.get(`${process.env.REACT_APP_API_URL}/auth/login/success`, {
withCredentials: true,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => {
console.log(res);
authenticate(res.data, () => {
isAuth && navigate("/");
});
})
.catch((err) => {
console.log(err);
// setError(err.response.data.errors);
});
And it works fine and perfectly on my local host, all other routes are working and even the other authentication method works fine. Why is this particular route been blocked by CORS? When I open the API URL ${process.env.REACT_APP_API_URL}/auth/login/success directly in the browser I can see the json data is been sent fine. What is going on? Please what am I doing wrong?
#Kaneki21 Solution actually worked for me but he has deleted his answer so I'm reposting it. All credits go to him, so this cors configuration solved the error:
const corsOptions = {
origin: [`${process.env.CLIENT_URL}`],
methods: "GET,HEAD,PUT,OPTIONS,POST,DELETE",
allowedHeaders: [
"Access-Control-Allow-Headers",
"Origin",
"X-Requested-With",
"Content-Type",
"Accept",
"Authorization",
"token",
"Access-Control-Request-Method",
"Access-Control-Request-Headers",
"Access-Control-Allow-Credentials",
],
credentials: true,
preflightContinue: false,
optionsSuccessStatus: 204,
};
app.use(cors(corsOptions));
But I ran into another issue, Although the request was sent now, I noticed cookies weren't included in the header even after I added withCredentials: true. Inspecting the request in the chrome network tab I could see the cookies was been filtered out. I solved that by switching from cookie-session to express-session and using this config:
app.set("trust proxy", 1); // trust first proxy
app.use(
session({
secret: process.env.JWT_SECRET,
resave: false,
saveUninitialized: true,
cookie: {
secure: true,
sameSite: "none",
},
})
);
Notice the sameSite: "none" in the cookie configuration, that enables cross-sites request and it requires secure to be true. Previously cookie-session wasn't setting the sameSite attribute even when I included it in the cookie config and browsers set Lax as a default so hence the switch to express-session. You can check out these sites to understand more site1 site2
I hope this helps someone out cause I know this gave me quite a bit of a headache
I have an Express server on which I'm generating a csrf token. My server looks like this
const csrfProtection = csrf({
cookie: {
httpOnly: true,
},
});
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
server.use(
cors({
origin: "http://localhost:3000",
credentials: true,
})
);
server.use(cookieParser());
server.use(csrfProtection);
...
//Other routes
and i'm sending the token like this
export const csrf = (req, res) => {
return res.send({ csrfToken: req.csrfToken() });
};
If I take it from the response and add it to the X-CSRF-Token header in Postman, then I can access all the routes just fine. But when I do it in React I always get the invalid csrf token error
This is how I take the token in React
export const getCSRFToken = async () => {
try {
const { data } = await axios.get("/auth/csrf");
axios.defaults.headers.post["X-CSRF-Token"] = data.csrfToken;
} catch (error) {}
};
And I'm using the withCredentials: true flag on other requests. I can't figure out what I'm missing.
Apparently the problem is that you need to pass the withCredetials flag to the request getting the csrf token too. So this fixed the problem.
export const getCSRFToken = async () => {
try {
const { data } = await axios.get("/auth/csrf", { withCredentials: true });
axios.defaults.headers.common["X-CSRF-Token"] = data.csrfToken;
} catch (error) {}
};
Maybe you should change axios.defaults.headers.post["X-CSRF-Token"] = data.csrfToken to axios.defaults.headers.common["X-CSRF-Token"] = data.csrfToken
I've a simple API in Express/Node and I also have a simple angular application for posting blogs. The only problem is when I hit the /contribute route using POST method. I'm getting this error on both chrome and firefox:
error: error { target: XMLHttpRequest, isTrusted: true, lengthComputable: false, … }
headers: Object { normalizedNames: Map(0), lazyUpdate: null, headers: Map(0) }
message: "Http failure response for localhost:3000/api/contribute: 0 Unknown Error"
name: "HttpErrorResponse"
ok: false
status: 0
statusText: "Unknown Error"
url: "localhost:3000/api/contribute"
: {…}
constructor: class HttpErrorResponse { constructor(init) }
: {…}
constructor: class HttpResponseBase { constructor(init, defaultStatus, defaultStatusText) }
: {…
Here's my server side code.
api.js
...
router.post('/contribute', (req, res) => {
console.log('Pushing new article');
let userPost = req.body;
let post = new Post(userPost);
post.save((error, registeredPost) => {
if (error) {
console.log(error);
} else {
res.status(200).send(registeredPost);
}
})
})
...
module.exports = router;
server.js
const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const api = require('./routes/api');
const cors = require('cors');
app.use(bodyParser.json());
// app.use(cors({ origin: 'http://localhost:4200' })); <--- TRIED THIS ALSO
app.use(function (req, res, next) {
// Website you wish to allow to connect
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:4200');
// Request methods you wish to allow
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
// Request headers you wish to allow
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type');
// Set to true if you need the website to include cookies in the requests sent
// to the API (e.g. in case you use sessions)
res.setHeader('Access-Control-Allow-Credentials', true);
// Pass to next layer of middleware
next();
});
app.use('/api', api);
app.get('/', function(req, res) {
res.send('Server is up and running!');
})
app.listen(3000, function() {
console.log('Server listening port:3000');
});
Yes, server is up and running.
Here is angular code.
auth.service.ts
private _contributeUrl = "https://localhost:3000/api/contribute";
...
pushNewPost(newPost) {
console.log("here is the new post", newPost); // GETTING CORRECT OUTPUT
return this._http.post<any>(this._contributeUrl, newPost);
}
contribute.component.ts
this._auth.pushNewPost(this.makeNewPost)
.subscribe (
res => {
(<HTMLInputElement>document.getElementById("inputTitle")).value="";
this.editorForm.reset();
this.addSingle();
},
err => console.log(err)
);
Now the fun part is that the same code is working perfectly when I make a post request to this route using Postman without any error.
Please correct my mistake. After adding:
pushNewPost(newPost) {
console.log("here is the new post", newPost);
let headers = new HttpHeaders({
'Content-Type': 'application/json',
});
let options = { headers: headers };
return this._http.post<any>(this._contributeUrl, newPost);
}
I'm getting this:
Seems like you are not sending in the headers from angular. Make the following changes:
pushNewPost(newPost) {
// adding the headers
const headers = new HttpHeaders({
'Content-Type': 'application/json',
});
const options = { headers: headers };
return this._http.post<any>(this._contributeUrl, newPost, options);
}
I'm running a vue app on an apache server on a virtual server. Express is started with nodemon.
When trying to login I'm getting a
Cannot read property 'status' of undefined xhr.js:160
POST https://143.93.46.35:60702/user/login net::ERR_TIMED_OUT
on chrome and
Cross-source (cross-origin) request blocked: The same source rule prohibits reading the external
resource on https://143.93.46.35:60702/user/login. (Reason: CORS request failed)
on firefox.
In vue my axios baseURL looks like (node is running on port 60702):
const apiClient = axios.create({
baseURL: `https://pvapp.umwelt-campus.de:60702`,
withCredentials: false, // This is the default
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
}
})
export default{
loginUser (user) {
return apiClient.post(`/user/login`, user)
},
...
}
And the user route on express is:
router.post('/login', async (req, res) => {
let compareUser = await db.query('SELECT * FROM app_users WHERE username=? LIMIT 1', [req.body.username]); // use db.query() to retrieve the password
if (compareUser.length < 1) // compareUser is an array with at most one item
res.sendStatus(403);
let valid = bcrypt.compareSync(req.body.password, compareUser[0].password);
if (!valid)
res.sendStatus(403);
let user = new User(compareUser[0]);
const token = jwt.sign({ user }, nconf.get('jwtToken'), { expiresIn: '14d' });
Object.assign(user, { token });
res.json(user);
});
In app.js cors is enabled like:
app.use(cors());
You need to res.setHeader('Access-Control-Allow-Origin', YourOrigin); in a middlewar before app.use(cors()); in order to allow the client.
EDIT :
You can do something like this :
router.use(function (req, res, next) {
let origin = req.headers.origin;
if (allowedOriginsTab.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
I am currently trying to set up a Node/Express app with a React client to interact with it. I setup passport to handle authentication with JWT. When the user logs in, I validate the email/password. I then set the cookie:
res.cookie('jwt', token, { httpOnly: true, secure: false });
I see the token being passed back in the response header, but when I inspect my Chrome browser's cookie under Developer Tools > Application > Cookies, I see an empty cookie. What am I doing wrong and how do I send the jwt in the response header with subsequent requests?
server/App.js
const app = express()
app.use(bodyParser.urlencoded({extended: true}));
app.use(bodyParser.json());
app.use(cookieParser());
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.post('/login', (req, res) => {
passport.authenticate('local', { session: false }, (error, user) => {
if (error || !user) {
res.status(400).json({ error });
}
// Construct JWT payload
const payload = {
email: user.email,
expires: Date.now() + parseInt(process.env.JWT_EXPIRATION_MS),
};
// Assign payload to req.user
req.login(payload, {session: false}, (error) => {
if (error) {
res.status(400).send({ error });
}
// Generate a signed JWT
const token = jwt.sign(JSON.stringify(payload), process.env.JWT_SECRET);
// Assign JWT to cookie
res.cookie('jwt', token, { httpOnly: true, secure: false });
res.status(200).send({ email: user.email });
});
})(req, res);
});
client/LoginModal.js
handleLogin = async () => {
const { name, email, password } = this.state
try{
const res = await axios.post('http://localhost:8080/login', {
email: email,
password: password,
})
if(res.status == 200){
console.log("Logged in")
console.log(res)
}
} catch (err) {
console.log(err)
}
}
Edit: My current workaround is to send the token as part of the payload. My react client then grabs the token from the payload and stores it in the browser's cookie. Is there a way to avoid this workaround (see below for example)?
server
res.status(200).send({ email: user.email, jwt: token });
client
if(res.status == 200){
cookies.set('jwt', res.data.jwt)
cookies.set('email', res.data.email)
}
When making the axis.post() call, you'll have to pass {withCredentials: true, credentials: 'include'} as your second argument, only this way will your browser set the cookies.
You have the cookie set with the httpOnly flag enabled. Most modern browsers restrict read access to such cookies through developer tools. You can read more about it here.
If you'd like to see the contents of the cookie in your development environment, set httpOnly to false.
This solution I found works with both local development and production ( and alos LAN access, eg. when you access the website on your LAN IP address such as http://192.168.xxx.xxx:<port>):
// Set CORS options
const cors = require(`cors`)
const whitelist = [
'http://localhost:<application port>', // not https
'https://yourprod.ip.address.com' // must be https!
'http://<your local IP>:<port>', // optional, LAN access
// ...
]
const corsOptions = {
credentials: true,
origin: (origin, callback) => {
// `!origin` allows server-to-server requests (ie, localhost requests)
if(!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error("Not allowed by CORS: "+ origin))
}
},
optionsSuccessStatus: 200
}
app.use(cors(corsOptions))
Then on the authentication endpoint:
// Set Cookie
const cookieContent = 'this is a cookie'
const cookieOptions = {
httpOnly: true, // safety, does not allow cookie to be read in the frontend javascript
maxAge: 24*3600*1, // cookie age in seconds
sameSite: 'Strict' // works for local development
}
if(process.env.NODE_ENV === 'production') {
// these options work on a https server
cookieOptions.secure = true
cookieOptions.sameSite= 'None'
}
res.cookie(
'cookie-tag',
refreshToken,
cookieOptions
)
res.json(cookieContent)
What worked for me is setting app.use(cors({ origin: true, credentials: true })) in cors package. Also setting withCredentials: true, credentials: 'include' while fetching from backend