I am currently making a Next.js app and I am having issues with cookies. I have an express API running on localhost:3001 which sets cookies when I signup/signin using express-cookie-session library. Whenever I do it through postman it works fine, however when I do it from next app an api it doesn't have "Set-Cookie" header in the response. I suspect it has to do with next app and express being on different ports and express being unable to set cookies to the next app however I'm unsure what to do about it. If it matters I wanted to set JWT's this way. It's possible to send them in response body but I would like to know how I could do it through the cookies.
Here are some relevant configurations:
app.use(cors());
app.set('trust proxy', true);
...
app.use(cookieSession({ signed: false, secure: false, sameSite: "lax" }));
a sign up controller:
const signUp = async (req: Request, res: Response, next: NextFunction) => {
...
const accessToken = TokenGenerator.generateAccessToken(user);
const refreshToken = TokenGenerator.generateRefreshToken(user);
user.refreshToken = refreshToken;
...
req.session = { accessToken, refreshToken };
res.send(user);
};
and getServerSideProps function
export const getServerSideProps = async (ctx) => {
const headers = ctx.req.headers;
const res = await axios.get("http://localhost:3001/users/current-user", {
headers,
});
return { props: { data: res.data } };
};
EDIT: Set-Cookie header is actually shown in chrome console however it isn't being console logged from axios response.
Here's example of cookie:
Set-Cookie: express:sess=eyJhY2Nlc3NUb2tlbiI6ImV5SmhiR2NpT2lKSVV6STFOaUlz
SW5SNWNDSTZJa3BYVkNKOS5leUpwWkNJNklqVm1abVEyTldSalpURXlaak5pT0RVellUWXlNR0
psT0NJc0ltVnRZV2xzSWpvaWJHdHNiaUlzSW1saGRDSTZNVFl4TURRME1qSXdOQ3dpWlhod0lq
b3hOakV3TkRReU1qRTVmUS5NN2szX1BVQy1hbzRQb2w4OXNiS05ndS1ndkpqNEVfUWdoX2RHSU
ZrZlZFIiwicmVmcmVzaFRva2VuIjoiZXlKaGJHY2lPaUpJVXpJMU5pSXNJblI1Y0NJNklrcFhW
Q0o5LmV5SnBaQ0k2SWpWbVptUTJOV1JqWlRFeVpqTmlPRFV6WVRZeU1HSmxPQ0lzSW1WdFlXbH
NJam9pYkd0c2JpSXNJbWxoZENJNk1UWXhNRFEwTWpJd05IMC5JdHA2WHh4aFRPMWJUc0oydGNM
ZU9hdFB3cWZWdWRsVmRQWkNnejB3eS1rIn0=; path=/; domain=http://localhost:3000.
I found a solution to this by adding this line to my server configuration:
app.use(cors({ origin: "http://localhost:3000", credentials: true }));
as well as setting withCredentials to true on my axios request.
Related
I am new to web development. At first, I created an authentication system with http protocol for both client dev-server and backend dev-server, which worked properly. However, I had to make the client dev-server secure to implement HLS video player. Therefore, now client side url is (https://localhost:15173/login), and backend url (http://localhost:3000). When client side url is (http://localhost:15173/login), cookie was generated on server-side and sent to the client side. So, I would like to know why this is happening.
Serverside: nodejs, express.js
Client side: javascript, vue3.js
Do I have to make both client side and backend https?
Here is backend code to generate cookie:
res.cookie('JWTcookie', accessToken, { httpOnly: true})
res.status(200).json(responseJson)
Here is backend code to validate cookie:
app.get("/login", function (req, res) {
var JWTcookie = req.cookies.JWTcookie;
console.log("JWT cookie is here", req.cookies.JWTcookie);
try {
console.log("veryfy token is here", verifyToken(JWTcookie));
const decoded = jwt.verify(JWTcookie, SECRET_KEY, function (err, decoded) {
return decoded;
})
const responseJson = {
success: true,
username: decoded.name,
userID: decoded.id
}
res.status(200).json(responseJson);
// console.log("decoded token ", decoded);
}
catch (err) {
const status = 401
const message = 'Unauthorized'
res.send("Not authorized. Better login");
// res.status(status).json({ status, message })
}
});
Here is client side code (vue.js) to send cookie to the sererside:
onMounted(() => {
const API_URL = "http://localhost:3000/";
const authStore = userAuthStore();
axios.get(API_URL + "login", { withCredentials: true }).then(res => {
if (res.data.success == true) {
const id = res.data.userID;
const username = res.data.username;
authStore.auth();
authStore.setUser(id, username);
console.log("mounted.")
router.push("/video");
}
else {
console.log("Response is here: ", res.data)
}
})
})
I believe the problem is the lack of understanding of how security system work when one of them is https and the other is http.
Any help would be appreciated. Thank you.
I tried to make the cookie secure by adding:
res.cookie('JWTcookie', accessToken, { httpOnly: true, secure: true})
But this didn't work.
When I gave both server side and client side https, it worked.
I would like to implement Csrf protection with NestJS and Quasar.
But I think I misunderstand something...
btw I'm not doing SSR, so I don't send the form from the back to the view.
Here is the NestJs back-end code:
async function bootstrap() {
const PORT = process.env.PORT;
const app = await NestFactory.create(AppModule, {
cors: true,
bodyParser: false,
});
console.log(`your App is listening on port ${PORT}`);
// Added Cookie-parser to user csurf packages
// Prevent CSRF attack
app.use(cookieParser());
app.use(csurf({ cookie: true }));
await app.listen(PORT);
}
bootstrap();
So I'm just using CookieParser and csurf package.
On my login page I call a "csrf endpoint" just to send a cookie to the view, to send it back with the post call (login).
I still get the "invalid csrf token" AND a CORS error and don't know why....(see screen below), any suggestions to make it works ?
When I try to login, error in the browser:
And error in the back-end:
Same error if I try a request with insomnia.
I thought that the CSRF token is attached to the "web browser" to go back to the back-end with nest request, so why I'm still getting this error ?
Insomnia send the cookie automatically with the right request so the token should go back to the back-end.
Any idea ?
Regards
EDIT:
After many times reading docs, It seems that CSRF protection is for SSR only ? No need to add csrf security with SPA ? Could anyone can confirm ?
EDIT: Here's another work:
The purpose here is to send a request before login to get a csrf token that I can put into a cookie to resend when I login with a POST method.
Here is my endpoint:
import { Controller, Get, Req, Res, HttpCode, Query } from "#nestjs/common";
#Controller("csrf")
export class SecurityController {
#Get("")
#HttpCode(200)
async getNewToken(#Req() req, #Res() res) {
const csrfToken = req.csrfToken();
res.send({ csrfToken });
}
}
Here is what I've done into my main.ts file (I'll explain below):
async function bootstrap() {
const PORT = process.env.PORT;
const app = await NestFactory.create(AppModule, {
cors: {
origin: "*",
methods: ["GET,HEAD,OPTIONS,POST,PUT"],
allowedHeaders: [
"Content-Type",
"X-CSRF-TOKEN",
"access-control-allow-methods",
"Access-Control-Allow-Origin",
"access-control-allow-credentials",
"access-control-allow-headers",
],
credentials: true,
},
bodyParser: false,
});
app.use(cookieParser());
app.use(csurf({ cookie: true }));
console.log(`your App is listening on port ${PORT}`);
await app.listen(PORT);
}
bootstrap();
And here my axiosInstance Interceptors of the request in my VueJS frontend:
axiosInstance.interceptors.request.use(
(req) => {
const token = Cookies.get('my_cookie')
if (token) {
req.headers.common['Authorization'] = 'Bearer ' + token.access_token
}
req.headers['Access-Control-Allow-Origin'] = '*'
req.headers['Access-Control-Allow-Credentials'] = 'true'
req.headers['Access-Control-Allow-Methods'] = 'GET,HEAD,OPTIONS,POST,PUT'
req.headers['Access-Control-Allow-Headers'] =
'access-control-allow-credentials,access-control-allow-headers,access-control-allow-methods,access-control-allow-origin,content-type,x-csrf-token'
const csrfToken = Cookies.get('X-CSRF-TOKEN')
if (csrfToken) {
req.headers['X-CSRF-TOKEN'] = csrfToken
console.log(req)
}
return req
},
(err) => {
console.log(err)
},
Here the same for repsonse:
axiosInstance.interceptors.response.use(
(response) => {
if (response?.data?.csrfToken) {
const {
data: { csrfToken },
} = response
Cookies.set('X-CSRF-TOKEN', csrfToken)
}
return response
},
And inside my login I make a call on the mounted function of my login component:
async mounted() {
const result = await securityService.getCsrf()
},
So now to explain:
As I said I'm not building a SSR project, that's why I want to send the token into a classic axios reponse and store it in a Cookie (this part is for test I heard that storing a csrf token into a classic cookie is not the right way.)
And for each next request I get the csrf token and "attach" it to the request into the headers, making my headers "custom".
Here is a problem I don't know how to make custom headers works with nestJS and CORS, that's why I try many thing with CORS options in NestJS and writte some custome header before the request go to the back-end but without success, I've got the same error message:
I'm a bit confuse about this problem and CORS/CSRF is a big deal for spa, my questions still the same, with CORS and SameSite cookie attributes, and my api is in a subdomain of my front-end, is it really necessary to make a anti-csrf pattern ?
Btw how can I make my custom headers working and why CORS say to me there is no "Access-Control-Allow-Origin" header but there is:
try to generate csrf token and pass to front on each petition
// main.ts - from NestJs - Backend
// after app.use(csurf({ cookie: true }))
app.use((req: any, res: any, next: any) => {
const token = req.csrfToken()
res.cookie("XSRF-TOKEN", token)
res.locals.csrfToken = token
next()
})
from: https://github.com/nestjs/nest/issues/6552#issuecomment-1175270849
I recently built a simple real-time chat application with Nextjs on the frontend and Express on the backend. The frontend is deployed on vercel while the backend is deployed on heroku. When a user logs into the app, the backend generates a jwt token which is then sent via an HttpOnly cookie back to the frontend. Here is the code for said response:
const authenticate = async (req, res, next) => {
userService
.authenticate(req)
.then((user) => {
const { token, ...userInfo } = user;
res
.setHeader(
"Set-Cookie",
cookie.serialize("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV !== "development",
maxAge: 60 * 60 * 24,
sameSite: "none",
path: "/",
})
)
.status(200)
.json(userInfo);
})
.catch(next);
};
After authentication, each subsequent request to the backend is supposed to send the token to ensure the user is logged in. For example, this is the request sent to the server to get a chat between the logged in user and another user.
const getChat = async (id) => {
const identification = id;
const response = await axios.get(
`<SERVER_URL>/chats/chat/${identification}`,
{ withCredentials: true }
);
return response;
};
In development when on localhost:3000 for the frontend and localhost:4000 for the backend, everything works fine. However, when I deployed the frontend to vercel and the backend to heroku, the browser simply refuses to save the cookie! The jwt token appears in the response header after sending the authentication request, but it isn't saved to the browser. I have tried absolutely everything I can think of, including changing the cookie parameters, but I can't get it to work. I am pretty sure I have cors properly configured on the backend as well, along with the cookie-parser module:
const cors = require("cors");
const cookieParser = require("cookie-parser");
app.use(
cors({
origin: "<CLIENT_URL>",
credentials: true,
})
app.use(cookieParser());
Thanks for taking the time to read this, any help would be greatly appreciated! And my apologies if I have not elaborated enough, this is my first post here and I'm still trying to learn the proper etiquette of the site!
HttpOnly can not read or write on client-side but when the first HttpOnly send through a request other request on the same origin can access the coockies in backend but you should request in Next.js like this.
Next.js using fetch :
const req = await fetch("http://localhost:7000/api/auth/login", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
body: JSON.stringify({
email: formData.get("email"),
password: formData.get("password"),
}),
});
const data = await req.json();
then in backend you can read the coockie through coockie-parser
server.js:
const cookieParser = require("cookie-parser");
app.use(coockieParser());
route.post('/login',(req,res) => {
if(user){
res
.cookie("access_token", newToken, {
httpOnly: true,
secure: process.env.NODE_ENV === "production" ? true : false,
})
.status(200)
.json({ ok: true, payload: data });
}
})
Now you can read this cookie in other routes but sure about the expiration time.
I'm doing fetch from my frontend to my express backend. But express logs req.cookies as '' (empty). I'm using cookieParser.
Why is express not finding the cookie, even though the browser shows the cookies being sent?
Note: I'm using cookies forwarded by my load balancer, which does the authentication and sends the session over.
Frontend
fetch(`${MY_URL}/logout`, {
credentials: 'include',
})
NodeJS
const cookieParser = require("cookie-parser");
app.use(cookieParser());
app.get("/logout", (req, res, next) => {
console.log(req.headers) // see below
console.log(JSON.parse(JSON.stringify(req.cookies))); // logs {}
console.log(JSON.parse(JSON.stringify(req.signedCookies))); // logs {}
// do stuff with cookie
});
Headers
{
...
cookie: ''
}
Cookie in Headers is an empty string
Network tab:
Got this working. Eventually the solution was that the Load balancer automatically forwards these headers to the backend silently. For my /logout api, instead of trying to grab the cookies from the headers, I set them regardless. Something like this:
app.get('/logout', (req, res) => {
res.cookie("AWSELBSessionCookie", "", {
maxAge: -1,
expires: Date.now(),
path: '/'
}
res.setHeader("Cache-Control", "must-revalidate, no-store, max-age=0");
res.setHeader("Pragma", "no-cache");
res.setHeader("Expires", -1);
res.redirect("https://my-login-page.com");
})
I am currently dealing with forms and decided to test sending an email onchange mid registration to the server and give responsive feedback to users.
On Vue component creation I get the csrf token and store it for future posts. I attach it to the headers as 'X-CSRF-Token'. I send the token and still receive the invalid CSRF token error. I have verified the data in headers and the csrf token is in-fact being sent but just being rejected or the header is missing something.
Screenshot of error and response
//App.js
var createError = require('http-errors');
var cors = require('cors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var csurf = require('csurf')
var Mongoose = require('mongoose')
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var productRouter = require('./routes/products')
var app = express();
// DB things
var db
dbConnect();
app.use(cors())
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser())
app.use(csurf({ cookie:true }))
app.use('/api/', indexRouter);
app.use('/api/users', usersRouter);
app.use('/api/products', productRouter)
//index routes
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.json({
message:'respond with a resource blank',
});
});
router.get('/getCSRF/', function(req, res, next) {
res.json({
csrf:req.csrfToken(),
});
});
module.exports = router;
Below is the route I try posting to
// Users Check Email Post Route
router.post('/checkEmail',function(req,res,next){
email = req.body.email
console.log(email)
User.findOne(function(err,user){
if (err) { return res.json({ err:err })}
else { return res.json({ user:true }) }
})
})
Here is the method used in Vue to post
checkEmail: function () {
var headers = {
'Accept': 'application/json',
'Access-Control-Allow-Origin': '*' ,
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type,
Accept',
'Content-Type':'application/x-www-form-urlencoded',
'X-CSRF-Token':this.$store.getters.getCSRF,
}
axios.post('http://localhost:8000/api/users/checkEmail', {
email: this.user.Email
},headers )
.then(function (response) {
console.log(response)
})
.catch(function (error) {
console.log(error.response)
});
},
The issue is pretty common and I have gone through over 20 posts and tried their solutions but it did not help at all. I've tried using csurf independently on the same route(didn't work). I've tried all manner of headers. Your help will be greatly appreciated .
EDIT: Uploading to show proper headers pre-post. Pre-post logs of data
So I realized that csurf looks for two things . A cookie (this is not very apparent from the error but nevertheless) and the token to be sent via header . Axios by default apparently does not support cookies so I decided to move to a JWT header approach.
Your way of sending the token via router.get('/getCSRF/', ...) does not seem very secure to me, as any attacker could also easily get this token via this get request.
If you use app.use(csurf({ cookie:true })), then Express will validate every POST/PUT/DELETE request based on a cookie, but you need to set this cookie yourself.
(Csurf sets a cookie named _csrf but this is not the actual CSRF token)
I got it working this way:
Nodejs:
app.use(cookieParser());
app.use(
csrf({
// compare the XSRF-TOKEN cookie with the X-XSRF-TOKEN header on every post request
cookie: {
secure: true,
sameSite: 'strict',
},
})
);
app.use(function (req, res, next) {
// set the XSRF-TOKEN cookie so the client can send it back in the X-XSRF-TOKEN header
res.cookie('XSRF-TOKEN', req.csrfToken());
next();
});
I used Nuxt as my client. Nuxt.config.js:
modules: [
'#nuxtjs/axios',
],
axios: {
credentials: true, // this will take the XSRF-TOKEN from the cookie and set it in the X-XSRF-TOKEN header
baseURL: nuxtBaseUrl,
browserBaseURL: nuxtBrowserBaseUrl,
},
publicRuntimeConfig: {
axios: {
credentials: true, // this will take the XSRF-TOKEN from the cookie and set it in the X-XSRF-TOKEN header
baseURL: nuxtBaseUrl,
browserBaseURL: nuxtBrowserBaseUrl,
},
},
Alternative without nuxt:
const instance = axios.create({
baseURL: 'https://my-server/',
withCredentials: true, // this will take the XSRF-TOKEN from the cookie and set it in the X-XSRF-TOKEN header
});
instance.post('http://my-server/api/users/checkEmail', {
email: this.user.Email
}