Lusca csrf not working as expected - http request using old token - node.js

I was trying to set up CSRF protection with lusca on my Express.js application. Not it looks like this:
this.app.use(lusca({
csrf: {
cookie: {name: '_csrf'}
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
nosniff: true,
referrerPolicy: "same-origin",
xframe: "SAMEORIGIN",
xssProtection: true,
}));
And on client side as follow:
const res = await axios.post(`${Constants.apiUrl()}/${Constants.paths.login}`,
credentials, {
withCredentials: true,
xsrfCookieName: '_csrf'
});
On the server-side, I also set some headers to be able to send cookies with request - res.header('Access-Control-Allow-Credentials', 'true').
Probably I'm missing some important part of how CSRF protection works. Now each time with the response, I'm getting new csrf token but this means that my new HTTP POST request sending the previous token that already outdated.
What I'm missing?

Finally, I found the problem after testing for 3 hours. You need to add the secret of the csrf. Also, if you are using Angular, you need to add angular: true into the csrf.
this.app.use(lusca({
csrf: {
cookie: {name: '_csrf'},
secret: 'qwerty'
},
hsts: { maxAge: 31536000, includeSubDomains: true, preload: true },
nosniff: true,
referrerPolicy: "same-origin",
xframe: "SAMEORIGIN",
xssProtection: true,
}));

Related

Why Nestjs not setting cookies in my browser?

I'm making an API in Nestjs that is consumed by an application in ReactJs. My problem is in the login route, when I use swagger, the cookie is saved in the browser normally but when I do a fetch from the front end, the cookie is not saved even though the response headers have the cookie.
I already tried to use all the sameSite options, I tried to put credentials include in the fetch but nothing works. If I log in to swagger first, then I try to do it in react, react copies the cookie that is saved in swagger.
For example, if in swagger I log in with user 1, and in react with the user 2, react steals the cookie from user 1 and ignores user 2 response cookie.
Code in react:
const res = await fetch(`${API_URL}/auth/login`, {
method: "POST",
headers: { "Content-type": "application/json", accept: "*/*" },
// credentials: "include",
body: JSON.stringify(data),
});
Main.ts:
const corsOptions = {
origin:
process.env.NODE_ENV === 'development' ||
process.env.MY_NODE_ENV === 'development'
? [process.env.PLATFORM_LOCAL_URL, process.env.LANDING_LOCAL_URL]
: [process.env.PLATFORM_PROD_URL, process.env.LANDING_PROD_URL],
credentials: true,
allowedHeaders: 'Content-Type, Accept, Origin',
preflightContinue: false,
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE',
};
app.enableCors(corsOptions);
app.use(helmet());
app.use(cookieParser());
Login Controller:
#UseGuards(LocalAuthGuard)
#Post('auth/login')
async login(
#Body() _: MakeAuthDto,
#Request() req,
#Res({ passthrough: true }) res,
) {
const access_token = await this.authService.login(req.user);
const cookiesOpts = {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'none',
path: '/',
maxAge: 60 * 60 * 24 * 3,
};
res.cookie('jwt', access_token, cookiesOpts);
return {
response: {
user: req.user,
expire: new Date().setDate(new Date().getDate() + 3),
},
};
}
Work on swagger:
After make request from ReactJs, the response cookies has the jwt:
But the cookie are not stored:
Looks like you're trying to set a cookie with the swagger editor.
See Note for Swagger UI and Swagger Editor users:
Cookie authentication is currently not supported for "try it out" requests due to browser security restrictions. See this issue for more information. SwaggerHub does not have this limitation.

React/Express set-cookie not working across different subdomains

I have a React frontend with Express API server, and I am trying to store a JWT in a secure httpOnly cookie for authorization. Below are the relevant parts of my code, which includes everything I've tried from countless Google/StackOverflow searches.
Am I missing something simple? I am trying to avoid storing the JWT in localStorage, but I am just at a loss right now. Nothing seems to work.
API (https://api.mydomain.com):
app.use(cors({
credentials: true,
exposedHeaders: ['SET-COOKIE'],
origin: 'https://staging.mydomain.com',
}));
app.post('/auth/login', (request, response) => {
...
response.cookie('jwt', JWT.sign({ id: user.id }, ...), {
domain: 'mydomain.com',
httpOnly: true,
sameSite: 'none',
secure: true,
});
response.json(user);
});
Web (https://staging.mydomain.com):
await fetch('https://api.mydomain.com/auth/login', {
body: JSON.stringify({ ... }),
credentials: 'include',
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
});
I see the set-cookie header in the response, but I do not see the cookie set in the developer tools and it is not passed in subsequent API requests.
Response Headers:
access-control-allow-credentials: true
access-control-allow-origin: https://staging.mydomain.com
access-control-expose-headers: SET-COOKIE
set-cookie: jwt=abcde*********.abcde*********.abcde*********; Domain=mydomain.com; Path=/; HttpOnly; Secure; SameSite=None
On your server, when you are setting the cookie, can you try adding a dot before the domain. The leading dot implies that the cookie is valid for subdomains as well.
response.cookie('jwt', JWT.sign({ id: user.id }, ...), {
domain: '.mydomain.com',
httpOnly: true,
sameSite: 'none',
secure: true,
});
When the client (here, https://staging.mydomain.com) will receive the Set-Cookie response header, with the trailing dot domain, it would accept the cookie since now the cookie is valid for subdomains too.
On a side note, I find Cookie Editor really helpful for debugging cookies.
Hope this answer helps you! :)

res.cookie is undefined when server uses HTTPS

I'm using a user login logic that saves JWT in cookies.
const token = jwt.sign({
username: login,
},
process.env.JWT_SECRET);
res.cookie("jwt", token, {
secure: false,
httpOnly: true,
});
console.log(res)
return res.status(200).json({
success: true,
username: login });
The frontend making requests runs oh HTTP. When the server runs also on HTTP, everything works fine, but when I go for HTTPS, the cookie is not saved at all. Setting 'secure' to true does not help. What can be the possible reason?
use secure: true,
in your response for cookie
I have same problem lately and here is my solution for this problem.
I change my res.cookie to this
res.cookie("jwt", token, {
secure: true,
httpOnly: true,
sameSite: "None", // I add this line
});

Can't make lusca CSRF work with https: 403 forbidden

This is driving me nuts. I have tried reading the lusca source code but found it hard to understand.
Checked several examples too, but since each config is different, and the only debugging output I have are two strings to compare, I'd better ask for some help!
Here's the code server side:
app.use([
cookieParser(process.env.SESSION_SECRET),
session({
resave: false,
saveUninitialized: true,
secret: process.env.SESSION_SECRET,
store: new MongoStore({ url: MONGO_URL, autoReconnect: true }),
cookie: {
secure: process.env.NODE_ENV === 'production'
},
}), lusca({
csrf: true,
xframe: 'SAMEORIGIN',
xssProtection: true,
})]);
And from the clientside, I send Ajax POST requests with the x-csrf-token:l0gH3xmssge53E/p2NsJ4dGnHaSLdPeZ+bEWs= header in it:
fetch(url, {
method: 'POST',
credentials: 'include',
headers: {
'x-csrf-token': CSRF_TOKEN
}
});
Crazy thing is, it's working locally, but as soon as I go https in production, I get the 403 Forbidden error message.
Here are the versions I use:
"cookie-parser": "1.4.3",
"express-session": "1.15.3",
"lusca": "1.5.1",
Also I read this from the express/session doc:
Note Since version 1.5.0, the cookie-parser middleware no longer needs to be used for this module to work.
But as far as I'm concerned, I need to store some persistent ID of the users (longer than the session). I need to use cookies for that, right?
I'd like to understand better on the whole session/cookie thing, but until now I never found any useful resource on the topic.
Thanks!
If you are running your Node.js server behind a proxy you will need to set trust proxy to true:
var isProductionEnv = process.env.NODE_ENV === 'production';
app.use([
cookieParser(process.env.SESSION_SECRET),
session({
resave: false,
saveUninitialized: true,
secret: process.env.SESSION_SECRET,
store: new MongoStore({ url: MONGO_URL, autoReconnect: true }),
proxy: isProductionEnv,
cookie: {
secure:isPrudictionEnv,
},
}), lusca({
csrf: true,
xframe: 'SAMEORIGIN',
xssProtection: true,
})]);
app.set('trust proxy', isProductionEnv);
Check out this stack overflow answer. Also check out this page on Express behind proxies.

Cross-domain sessions with connect-mongo

I have built Node.js app with Express 4, for manage sessions I use connect-mongo middleware, all works.
But I need login to my app from another site.
App is hosted on aws EC2.
I use SalesForce and after login to it, I want open my app, but DON'T want input credentials...
On node.js server I have added headers:
res.setHeader('Access-Control-Allow-Origin', req.headers.origin);
res.setHeader('Access-Control-Allow-Credentials', 'true');
In SF, onClick button I execute:
jsonData = {
"email": 'test1#example.com',
"password": "test"
}
$.ajaxSetup({
type: "POST",
data: {},
dataType: 'json',
xhrFields: {
withCredentials: true
},
crossDomain: true
});
$.post( 'http://ec2-someip.us-west-2.compute.amazonaws.com//login', jsonData)
.done(function( data ) {
console.log( "done" );
console.log(data);
//redirect to data url
})
.fail(function(data) {
console.log( "error" );
console.log( "data" );
});
Node.js returns me correct data url, but doesn't add session cookie, and that's why I see login page after redirect...
When I manually send POST request from browser (I use "Rest Console" app for Google Chrome), node.js added cookie.
What is wrong?
There is a way to login from SF (or any other site) ?
Thank you.
Fixed by adding cookie domain settings:
app.use(session({
secret: config.get('session:key'),
saveUninitialized: true,
resave: true,
store: new MongoStore({
db: mongoose.connection.db
}),
cookie: {
path: '/',
domain: utils.isDevelopmentEnv() ? null : '.' + config.get('domain').replace('http://', '').replace('https://', ''),
httpOnly: true
}
}));

Resources