Invalid csrf token with NestJS - vue.js

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

Related

https clientside and http back end cookie is not sent

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.

HttpOnly cookie appears in response header but is not being saved to the browser

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.

Cookies from separate api with Next.js

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.

Vue axios doesnt change header

I am quite new to vue and I am trying to send a request to my api using axios.
I build an interceptor which seems to work (logging is happening)
export default function setup() {
console.log('Http interceptor starting...')
Axios.interceptors.request.use((request) => {
const token = store.getters.token;
if (token) {
request.headers.Authorization = `Bearer ${token}`;
}
console.log(request);
return request
}, (err) => {
return Promise.reject(err);
});
}
If I check the console I can see the request including the token. If I check my network tab in the browser i can see the same request without the token. If I check the console of my api the token is null. Any Ideas?
Edit: If I use postman with the same request and the same token it is working as it shuld

How to properly use passport-github for REST API authentication?

I am building a vue.js client which needs to be authenticated through github oauth using an express server. It's easy to do this using server side rendering but REST API has been troublesome for me.
I have set the homepage url as "http://localhost:3000" where the server runs and I want the authorization callback url to be "http://localhost:8080" (which hosts the client). I am redirecting to "http://localhost:3000/auth/github/redirect" instead, and in its callback redirecting to "http://localhost:8080". The problem I am facing is that I am unable to send user data to the vuejs client through res.redirect. I am not sure if I am doing it the right way.
router.get("/github", passport.authenticate("github"));
router.get(
"/github/redirect",
passport.authenticate("github", { failureRedirect: "/login" }),
(req, res) => {
// res.send(req.user);
res.redirect("http://localhost:8080/"); // req.user should be sent with this
}
);
I have implemented the following approach as a work around :-
A route that returns the user details in a get request :
router.get("/check", (req, res) => {
if (req.user === undefined) {
res.json({});
} else {
res.json({
user: req.user
});
}
});
The client app hits this api right after redirection along with some necessary headers :
checkIfLoggedIn() {
const url = `${API_ROOT}auth/check/`;
return axios(url, {
headers: { "Content-Type": "application/json" },
withCredentials: true
});
}
To enable credentials, we have to pass the following options while configuring cors :
var corsOption = {
origin: true,
credentials: true
};
app.use(cors(corsOption));