Express session not persisting between api calls from react front end - node.js

Before asking this, I did have a look at other similar questions, but none of them have been of help as of yet.
I have a react front-end using axios to make api calls to a node backend using express and express session. Once I enter login info on my front end, I send it to my backend where I set a session and return a response to the front end that I set in the localStorage to use for client side validation.
When I try to access a protected api endpoint - from the front end - after logging in, the session does not have any of the information I set in it and thus, gives me an error. However, if I try to login and access the protected endpoint from postman, it works perfectly fine.
Here is my express-session config:
router.use(session({
secret: 'notGoingToWork',
resave: false,
saveUninitialized: true
}))
Here is the api call (after logging in) I am making through axios:
axios.get(`http://localhost:5000/users/personNotes/${JSON.parse(userData).email}`)
.then(response => console.log(response);
I do not know what the issue is and would appreciate any help. Thank you in advance!

try using withCredentials
axios(`http://localhost:5000/users/personNotes/${JSON.parse(userData).email}`, {
method: "get",
withCredentials: true
})
or
axios.defaults.withCredentials = true

You can use axios try withCredentials to true.
For fetch with credentials to include will also work.
fetch(URL,
{
credentials: 'include',
method: 'POST',
body: JSON.stringify(payload),
headers: new Headers({
'Content-Type': 'application/json'
})
})

To use fetch with
credentials: 'include'
I also had to add the following in Express App.js.
To note, 'Access-Control-Allow-Origin' cannot set to '*' with credentials. It must use a specific domain name.
res.setHeader(
'Access-Control-Allow-Origin',
'http://localhost:3000'
);
res.setHeader('Access-Control-Allow-Credentials', 'true');

Related

Browser is not receiving and not sending cookies with requests - React + Nestjs

I'm generating JWT token on my Nestjs backend which then I try to send with cookie to my frontend React application in order to know which user is logged in.
Problem is that I'm not receiving this cookie in browser, and it's not automatically added to other requests.
I'm sending response inside my service like this:
async login(loginData: UserLoginInterface, res: Response) {
...
return res.status(200).cookie('jwt', token.accessToken, {
secure: false,
domain: 'localhost',
httpOnly: false,
}).json(userResponse);
}
At this point I know the token is generated, it's saved in DB.
But I can't see this cookie, or any other cookie I try in my browser:
Doesn't matter if the httpOnly flag is true or false.
And then, when I try to call action that is restricted only for logged in user, which have the jwt token in request, then Nest is throwing 401 UnauthorizedException
So at this point I know that it's not sent automatically with request as I read in other thread like this:
Why browser is not setting the cookie sent from my node js backend?
But when I make this POST request from Postman.
Then I can see that cookie is sent properly and I can read the JWT token:
Along with headers:
And also it works fine when I call the function that is restricted only to authorized users.
Here is my bootstrap in main.ts:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: 'http://localhost:3000'
});
app.getHttpAdapter().getInstance().disable('x-powered-by');
app.use(cookieParser());
await app.listen(process.env.PORT || 3500);
}
bootstrap();
After some time of debugging I found out that it's not the browser that is ignoring properly sent cookie, and in fact it is backend that is not sending the cookie to the browser client.
And the thing was about how the request is being send.
I've found this thread to be useful:
Express doesn't set a cookie
In my case setting flag withCredentials: true in axios was sufficient.
const API = axios.create({
baseURL: 'http://localhost:5000',
withCredentials: true,
});
EDIT
Also, seems like the way I send response also matters, the code above is not sending cookie properly to the browser for some reason, but this works fine:
res.status(200).cookie('jwt', token.accessToken);
return res.json(userResponse);

Get cookie on front-end

I use on back-end nodejs(nestjs). From the server i send the cookies after login:
res.cookie('token', 'token', {
httpOnly: true
});
And this is my cors setting app.enableCors({credentials: true });;
As front-end i use reactjs. After login the server sent the cookies here:
But i need to get the cookies here:
Why i don't get the cookies in the place where i showed and how to get them there to save them even o reloading the page?
The reason the cookie is not persisted in the frontend is that you are probably not setting the withCredentials on your frontend request. An example with axios would be:
axios.post('http://localhost:3001', {}, { withCredentials: true })
An example with fetch is:
fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
credentials: 'include'
}
Note: For security reasons you must have explicitly specified the origin on your backend CORS configuration otherwise you will get the following error:
Access to XMLHttpRequest at 'http://localhost:3001/' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute.
To do that with nest.js/express you can pass:
app.enableCors({
credentials: true,
origin: ['http://localhost:3002', 'your-production-domain']
});

Authenticate from onelogin and redirect to dashboard

I have an app with react/redux frontend and express backend which I created using create-react-app. I want to do authentication using onelogin and passportjs. Following this guide (https://developers.onelogin.com/quickstart/authentication/nodejs) I was able to setup everything, the issue is, my express server is running on port 5000, but the react app runs on port 3000.
If I go to localhost:3000/login, nothing happens, and If I go to localhost:5000/login I do get authenticated with onelogin, but nothing is ever returned, and it just gets stuck.
This is my /login code
app.get('/login', passport.authenticate('openidconnect', {
successReturnToOrRedirect: "/",
scope: 'profile',
}));
And this is the callback
app.get('/oauth/callback', passport.authenticate('openidconnect', {
callback: true,
successReturnToOrRedirect: '/',
failureRedirect: '/login'
}));
Here's the full code (https://github.com/onelogin/onelogin-oidc-node/blob/master/1.%20Auth%20Flow/app.js)
I understand why this is all happening, since my express server doesn't render anything. But how can I handle this?
Redirecting to localhost:3000/create instead of /create fixed it. Not sure if best practice.
app.get('/login', passport.authenticate('openidconnect', {
successReturnToOrRedirect: "localhost:3000/create",
scope: 'profile',
}));
app.get('/oauth/callback', passport.authenticate('openidconnect', {
callback: true,
successReturnToOrRedirect: 'http://localhost:3000/create',
failureRedirect: 'http://localhost:3000/login'
}));
you have to proxy your request to the backend add this line to your packages.json
"proxy": "http://127.0.0.1:5000/"
how are you making your request to authenticate from the front end? when doing authentication with react I use react-router and then use axios to authenticate
so when user submits the login form I make a post request to the login route to return a JWT (I have not used passport before). All future requests from the client I include the jwt to validate the user as authenticated.
import Cookies from "js-cookie";
import QueryString from "query-string";
import axios from 'axios';
let myUrl = "login/";
let myData = QueryString.stringify({
...this.state.form,
});
axios.post(myUrl, myData,{
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"cache-control": "no-cache",
},
})
.then((response) => {
// store token in cookies
// redirect to authenticated route
Cookies.set('token', response.data.token, { expires: 1/24 })
})
.catch((error) => {
//handle displaying error to user here
})
if you are not using react router or are trying to serve data from your backend as apposed to retrieving it from the frontend please let me know and I will change my post to fit your authentication flow

express JS cors middleware does not work when server is accessed though fetch request

Server is on http://localhost:3001, and client is same, but with port 3000.
On client I run simple fetch, I need to get logged-in user data from server, and currently I am doing it just using GET method(GET, POST, none work) like this(I also have to include cookies):
fetch("http://localhost:3001/user", {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json"
}
}).then(response => {
console.log(response);
});
And the server:
const cors = require("cors");
var corsOptions = {
origin: "http://localhost:3000",
optionsSuccessStatus: 200
};
app.get("/user", cors(corsOptions), function(req, res, next) {
u_session = req.session;
const db_result = db
.collection("users")
.findOne({ email: u_session.email }, function(err, result) {
if (err) throw err;
res.json({ email: result.email, type: result.type });
});
});
What I get is cors error:
Access to fetch at 'http://localhost:3001/user' from origin 'http://localhost:3000' 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. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
Also if i go to server URL through browser, I can see access-allow-control-allow-origin header set successfully.
Also as requested, screenshot of failed case network tab:
I've searched plenty of solutions on the internet, nothing seems to work. Am I missing something?
Thanks for any help.
Ensure that if you have a proxy set that it is set to http://localhost:3001. After that adjust your fetch to only use a partial url like so:
fetch("/user", {
method: "GET",
headers: {
"Content-Type": "application/json"
}
}).then(response => {
console.log(response);
});
it should be safe to remove this as well:
const cors = require("cors");
var corsOptions = {
origin: "http://localhost:3000",
optionsSuccessStatus: 200
};
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
What sticks out to me is "preflight". I think it may be the OPTIONS request that doesn't have the correct CORS headers enabled. Ensure that you're enabling CORS on GET, POST, OPTIONS, and any other method your API supports.
Since you send credentials from the client, you must configure your cors module to allow credentials via athecredentials property. Also, application/json is a non-simple Content-Type, so you must allow that explicitly via allowedHeaders:
var corsOptions = {
origin: "http://localhost:3000",
optionsSuccessStatus: 200,
credentials: true,
allowedHeaders: ["Content-Type"]
};
Without this, the server will not include a Access-Control-Allow-Credentials header in the OPTIONS preflight, and the browser will refuse to send the main GET request.

Express doesn't set a cookie

I have problem with setting a cookies via express. I'm using Este.js dev stack and I try to set a cookie in API auth /login route. Here is the code that I use in /api/v1/auth/login route
res.cookie('token', jwt.token, {expires: new Date(Date.now() + 9999999)});
res.status(200).send({user, token: jwt.token});
In src/server/main.js I have registered cookie-parser as first middleware
app.use(cookieParser());
The response header for /api/v1/auth/login route contains
Set-Cookie:token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ..
but the cookie isn't saved in browser (document.cookie is empty, also Resources - Cookies tab in develepoers tools is empty) :(
EDIT:
I'm found that when I call this in /api/v1/auth/login (without call res.send or res.json)
res.cookie('token', jwt.token, {expires: new Date(Date.now() + 9999999), httpOnly: false});
next();
then the cookie is set AND response header has set X-Powered-By:Este.js ... this sets esteMiddleware in expres frontend rendering part.
When I use res.send
res.cookie('token', jwt.token, {expires: new Date(Date.now() + 9999999), httpOnly: false}).send({user, token: jwt.token});`
next();
then I get error Can't set headers after they are sent. because send method is used, so frontend render throw this error.
But I have to send a data from API, so how I can deal with this?
I had the same issue. The server response comes with cookie set:
Set-Cookie:my_cookie=HelloWorld; Path=/; Expires=Wed, 15 Mar 2017 15:59:59 GMT
But the cookie was not saved by a browser.
This is how I solved it.
I use fetch in a client-side code. If you do not specify credentials: 'include' in fetch options, cookies are neither sent to server nor saved by a browser, although the server response sets cookies.
Example:
var headers = new Headers();
headers.append('Content-Type', 'application/json');
headers.append('Accept', 'application/json');
return fetch('/your/server_endpoint', {
method: 'POST',
mode: 'same-origin',
redirect: 'follow',
credentials: 'include', // Don't forget to specify this if you need cookies
headers: headers,
body: JSON.stringify({
first_name: 'John',
last_name: 'Doe'
})
})
Struggling with this for a 3h, and finally realized, with axios, I should set withCredentials to true, even though I am only receiving cookies.
axios.defaults.withCredentials = true;
I work with express 4 and node 7.4 and Angular, I had the same problem this helped me:
a) server side: in file app.js I give headers to all responses like:
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', req.headers.origin);
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
This must have before all routers.
I saw a lot of added this header:
res.header("Access-Control-Allow-Headers","*");
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
but I don't need that.
b) when you define cookie you need to add httpOnly: false, like:
res.cookie( key, value,{ maxAge: 1000 * 60 * 10, httpOnly: false });
c) client side: in send ajax you need to add: withCredentials: true, like:
$http({
method: 'POST',
url: 'url',
withCredentials: true,
data : {}
}).then(function(response){
// do something
}, function (response) {
// do something else
});
There's a few issues:
a cookie that isn't explicitly set with httpOnly : false will not be accessible through document.cookie in the browser. It will still be sent with HTTP requests, and if you check your browsers' dev tools you will most likely find the cookie there (in Chrome they can be found in the Resources tab of the dev tools);
the next() that you're calling should only be used if you want to defer sending back a response to some other part of your application, which—judging by your code—is not what you want.
So, it seems to me that this should solve your problems:
res.cookie('token', jwt.token, {
expires : new Date(Date.now() + 9999999),
httpOnly : false
});
res.status(200).send({ user, token: jwt.token });
As a side note: there's a reason for httpOnly defaulting to true (to prevent malicious XSS scripts from accessing session cookies and the like). If you don't have a very good reason to be able to access the cookie through client-side JS, don't set it to false.
I had the same issue with cross origin requests, here is how I fixed it. You need to specifically tell browser to allow credentials. With axios, you can specify it to allow credentials on every request like
axios.defaults.withCredentials = true
however this will be blocked by CORS policy and you need to specify credentials is true on your api like
const corsOptions = {
credentials: true,
///..other options
};
app.use(cors(corsOptions));
Update: this only work on localhost
For detail answer on issues in production environment, see my answer here
I was also going through the same issue.
Did code changes at two place :
At client side :
const apiData = await fetch("http://localhost:5000/user/login",
{
method: "POST",
body: JSON.stringify(this.state),
credentials: "include", // added this part
headers: {
"Content-Type": "application/json",
},
})
And at back end:
const corsOptions = {
origin: true, //included origin as true
credentials: true, //included credentials as true
};
app.use(cors(corsOptions));
Double check the size of your cookie.
For me, the way I was generating an auth token to store in my cookie, was causing the size of the cookie to increase with subsequent login attempts, eventually causing the browser to not set the cookie because it's too big.
Browser cookie size cheat sheet
There is no problem to set "httpOnly" to true in a cookie.
I am using "request-promise" for requests and the client is a "React" app, but the technology doesn't matter. The request is:
var options = {
uri: 'http://localhost:4000/some-route',
method: 'POST',
withCredentials: true
}
request(options)
.then(function (response) {
console.log(response)
})
.catch(function (err) {
console.log(err)
});
The response on the node.js (express) server is:
var token=JSON.stringify({
"token":"some token content"
});
res.header('Access-Control-Allow-Origin', "http://127.0.0.1:3000");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header( 'Access-Control-Allow-Credentials',true);
var date = new Date();
var tokenExpire = date.setTime(date.getTime() + (360 * 1000));
res.status(201)
.cookie('token', token, { maxAge: tokenExpire, httpOnly: true })
.send();
The client make a request, the server set the cookie , the browser (client) receive it (you can see it in "Application tab on the dev tools") and then I again launch a request to the server and the cookie is located in the request: "req.headers.cookie" so accessible by the server for verifying.
I had same problem in Angular application. The cookies was not set in browser although I used
res.cookie("auth", token, {
httpOnly: true,
sameSite: true,
signed: true,
maxAge: 24 * 60 * 60 * 1000,
});
To solve this issue, I added app.use(cors({ origin:true, credentials:true })); in app.js file of server side
And in my order service of Angular client side, I added {withCredentials: true} as a second parameter when http methods are called like following the code
getMyOrders() {
return this.http
.get<IOrderResponse[]>(this.SERVER_URL + '/orders/user/my-orders', {withCredentials: true})
.toPromise();}
vue axios + node express 2023
server.ts (backend)
const corsOptions = {
origin:'your_domain',
credentials: true,
optionSuccessStatus: 200,
}
auth.ts (backend)
res.cookie('token', JSON.stringify(jwtToken), {
secure: true,
httpOnly: true,
expires: dayjs().add(30, "days").toDate(),
sameSite: 'none'
})
authService.ts (frontend)
export class AuthService {
INSTANCE = axios.create({
withCredentials: true,
baseURL: 'your_base_url'
})
public Login = async (value: any): Promise<void> => {
try {
await this.INSTANCE.post('login', { data: value })
console.log('success')
} catch (error) {
console.log(error)
}
}
it works for me, the cookie is set, it is visible from fn+F12 / Application / Cookies and it is inaccessible with javascript and the document.cookie function. Screenshot Cookies Browser
One of the main features is to set header correctly.
For nginx:
add-header Access-Control-Allow-Origin' 'domain.com';
add_header 'Access-Control-Allow-Credentials' 'true';
Add this to your web server.
Then form cookie like this:
"cookie": {
"secure": true,
"path": "/",
"httpOnly": true,
"hostOnly": true,
"sameSite": false,
"domain" : "domain.com"
}
The best approach to get cookie from express is to use cookie-parser.
A cookie can't be set if the client and server are on different domains. Different sub-domains is doable but not different domains and not different ports.
If using Angular as your frontend you can simply send all requests to the same domain as your Angular app (so the app is sending all API requests to itself) and stick an /api/ in every HTTP API request URL - usually configured in your environment.ts file:
export const environment = {
production: false,
httpPhp: 'http://localhost:4200/api'
}
Then all HTTP requests will use environment.httpPhp + '/rest/of/path'
Then you can proxy those requests by creating proxy.conf.json as follows:
{
"/api/*": {
"target": "http://localhost:5200",
"secure": false,
"changeOrigin": true,
"pathRewrite": {
"^/api": ""
}
}
}
Then add this to ng serve:
ng serve -o --proxy-config proxy.conf.json
Then restart your app and it should all work, assuming that your server is actually using Set-Cookie in the HTTP response headers. (Note, on a diff domain you won't even see the Set-Cookie response header, even if the server is configured correctly).
Most of these answers provided are corrections, but either of the configuration you made, cookies won't easily be set from different domain. In this answer am assuming that you are still in local development.
To set a cookie, you can easily use any of the above configurations or
res.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']); // setting multiple cookies or
res.cookie('token', { maxAge: 5666666, httpOnly: true })
Both of the will set your cookie while to accessing your cookie from incoming request req.headers.
In my case, my cookie were not setting because my server was running on http://localhost:7000/ while the frontend was running on http://127.0.0.1:3000/ so the simple fix was made by making the frontend run on http://localhost:3000 instead.
I struggle with it a lot so follow below solution to get through this
1 check if you are getting token with response with postmen in my case i was getting token in postmen but it wasn't being saved in cookies.
I was using a custom publicRequest which looks like below
try {
const response = await publicRequest.post("/auth/login", user, {withCredentials: true});
dispatch(loginSuccess(response.data));
} catch (error) {
dispatch(loginFail());
dispatch(reset());
}
I was using this method in other file to handle login
I added {withCredentials: true} in both methods as option and it worked for me.
I am late to the party but nothing fixed it for me. This is what I was missing (and yeah, it's stupid):
I had to add res.send() after res.cookie() - so apperently sending a cookie is not enough to send a response to the browser.
res.cookie("testcookie", "text", cookieOptions);
res.send();
You have to combine:
including credentials on the request with, for example withCredentials: true when using axios.
including credentials on the api with, for example credentials: true when using cors() mw.
including the origin of your request on the api, for example origin: http://localhost:3000 when using cors() mw.
app.post('/api/user/login',(req,res)=>{
User.findOne({'email':req.body.email},(err,user)=>{
if(!user) res.json({message: 'Auth failed, user not found'})
user.comparePassword(req.body.password,(err,isMatch)=>{
if(err) throw err;
if(!isMatch) return res.status(400).json({
message:'Wrong password'
});
user.generateToken((err,user)=>{
if(err) return res.status(400).send(err);
res.cookie('auth',user.token).send('ok')
})
})
})
});
response
res.cookie('auth',user.token).send('ok')
server gives response ok but the cookie is not stored in the browser
Solution :
Add Postman Interceptor Extension to chrome which allows postman to store cookie in browser and get back useing requests.

Resources