Invalid CSRF Token in React but valid in Postman - node.js

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

Related

Frontend not receiving cookie from backend (ExpreeJS, Passport, Postgresql)

I have been struggling on this problem for days. I have a NextJS frontend running on localhost:3000 and an ExpressJS backend running on localhost:3001.
I am trying to build a login flow where I send an axios login request to the backend, authenticate with passport, and send the cookie back to the frontend (that can subsequently be used).
When I run through the flow, I can successfully send data to the backend and authenticate with passport (which writes a row to my session table), and redirect on the frontend. However, I do not see the cookie in my frontend browser (Inspect Element > Application > Cookies > localhost:3000). And when I am redirected to my dashboard page, I show as unauthorized from my status endpoint (which I believe means the cookie is not being set correctly). When I hit the backend endpoint with Postman, I can see the cookie is successfully sent and the subsequent /status endpoint call returns as authorized.
Can anyone help me understand why my cookies aren't being set correctly?
Backend - Express Setup:
const app = express ()
// Enable parsing middleware for requests
app.use(express.json())
app.use(express.urlencoded({ extended: false }))
// Enable CORS
const originURL = process.env.RAILWAY_STATIC_FRONTEND_URL ? process.env.RAILWAY_STATIC_FRONTEND_URL : process.env.LOCAL_STATIC_FRONTEND_URL || 'http://localhost:3000'
app.use(cors({
origin: [originURL],
credentials: true
}))
// Session store
const pgSession = require('connect-pg-simple')(session);
const postgreStore = new pgSession({
// check interface PGStoreOptions for more info https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/connect-pg-simple/index.d.ts
// pool: poolInstance,
createTableIfMissing: true, // this will create a `session` table if you do not have it yet
})
// 1000ms * 60seconds * 60min * 24hrs * 7days = 1 week
const maxAge = 1000 * 60 * 60 * 24 * 7
app.use(session({
secret: process.env.EXPRESS_SESSION_SECRET || 'secret',
resave: false,
saveUninitialized: false,
cookie: {
maxAge: maxAge,
sameSite: "none",
secure: false,
httpOnly: false
},
store: postgreStore,
}))
// Enable Passport
app.use(passport.initialize())
app.use(passport.session())
// Prefix all backend routes with '/api'
app.use('/api', routes)
Backend - Login + Status Routes:
import passport from 'passport';
import bcrypt from 'bcryptjs';
import { PrismaClient } from '#prisma/client';
import { Router } from "express";
const router = Router ();
const prisma = new PrismaClient();
router.post('/login', passport.authenticate('local'), (req, res) => {
return res.sendStatus(200)
})
router.get('/status', (req, res) => {
return req.user
? res.send(req.user)
: res.status(401).send({ msg: "Unauthorized" })
})
export default router
Frontend: Login API Call
import type { NextApiRequest, NextApiResponse } from 'next'
import axios from "axios"
type ResponseData = {
message: string
}
export default async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
const BACKEND_API_URL = process.env.RAILWAY_STATIC_BACKEND_URL ? process.env.RAILWAY_STATIC_BACKEND_URL : process.env.NEXT_PUBLIC_LOCAL_STATIC_BACKEND_URL
const headers = {
}
const inputs = {
username: req.body.username,
password: req.body.password
}
if (!headers) return res.redirect(302, '/')
const config = {
headers: headers,
withCredentials: true
}
try {
// const { data } = await axios.post(`${BACKEND_API_URL}/api/auth/login`, inputs, { withCredentials: true })
await axios.post(`${BACKEND_API_URL}/api/auth/login`, inputs, { withCredentials: true })
return res.redirect(307, '/dashboard')
} catch (err) {
console.log(err)
return res.redirect(302, '/login')
}
}
Frontend - Dashboard Page
import { GetServerSidePropsContext, NextPage } from 'next'
import axios from 'axios'
type PageProps = {
msg: String,
}
const DashboardPage: NextPage<PageProps> = ({ msg }) => {
console.log(msg)
return (
// <div className={styles.container}>
<div>
<div> Dashboard Page </div>
<div> { msg } </div>
</div>
)
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const BACKEND_API_URL = process.env.RAILWAY_STATIC_BACKEND_URL ? process.env.RAILWAY_STATIC_BACKEND_URL : process.env.NEXT_PUBLIC_LOCAL_STATIC_BACKEND_URL
const headers = {
}
const config = {
headers: headers,
withCredentials: true
}
let msg
try {
const user = await axios.get(`${BACKEND_API_URL}/api/auth/status`, { withCredentials: true })
console.log(user)
msg = user
} catch (err) {
// console.log(err)
console.log(err.response.data)
msg = err.response.data.msg
}
var response = {
props: {
msg
}
}
return response
}
export default DashboardPage
It is basically because the client side is not storing the cookie. It is an issue from backend. There will be different settings while running the project in local and in cloud.
Try tweaking like this in your session settings and check in local environment. These worked for my local environment.
{ httpOnly: true, sameSite: 'None', secure: true, }
you have to tweak this in trial and error method for cloud hosting in railway.

req.cookies not being send to the back-end for JWT authentication

I know this question has been asked a 1000 times on Stack Overflow but none of the solutions seem to fit mine. I've also been through a lot of GitHub repo's but I just can't seem to find the solution. Also this is my first experience with developing a back-end.
The problem
I created a backend with authentication including refresh token and refresh token rotation. When I try to log in / sign up / request a request token in Insomnia I don't have any issues at all. Within MongoDB my refreshToken gets updated, I retrieve a new access and refreshtoken after calling the /refresh endpoint. However when I try to do call the refresh endpoint from the browser, I get an error "401 unauthorized" on the first step because it does not see the cookie. When trying to log the cookie I get [Object: null prototype] {}.
I know I am in development so what I've already tried:
Set withCredentials: true for the axios calls
Added credentials: true and origin: true to the cors config
Setting secure: false for res.cookie()
When I try this from my React front-end I do bump into some issues.
- Login endpoint properly sends the accessToken in the json and the refreshToken as a HTTPOnly Cookie
- Response from backend when calling the refresh endpoint
When I try to console.log the req.cookies I get [Object: null prototype] {}.
Here are some parts of my code:
Server.js
require("dotenv").config({ path: "./.env" });
const express = require("express");
const app = express();
const connectDB = require("../server/configs/db");
const errorHandlerMiddleware = require("../server/middleware/error-handler");
const mongoose = require("mongoose");
const cors = require("cors");
const path = require("path");
const PORT = process.env.PORT || 5000;
const verifyJWT = require("./middleware/verifyJWT");
const cookieParser = require("cookie-parser");
//Connect Database
connectDB();
const db = mongoose.connection;
// Middleware
app.use(cors({ credentials: true, origin: true }));
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(errorHandlerMiddleware);
//Serve public folder
app.use("/", express.static(path.join(__dirname, "/public")));
//Routes
app.use("/api/users", require("./routes/authRoutes"));
app.use("/api/logout", require("./routes/logoutRoutes"));
//Protected routes
app.use(verifyJWT);
app.use("/api/habitcards", require("./routes/habitcardRoutes"));
db.once("open", () => {
console.log("Connected to database");
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
});
// Log errors on occurence
db.on("error", (err) => {
console.log("Error connecting to database", err);
});
The refresh controller
//Get the refresh token from the request
const cookies = req.cookies;
const refreshToken = cookies.jwt;
console.log(cookies);
console.log("step");
//Check if the refresh token is present
if (!refreshToken) {
return res.status(401).json({ message: "Unauthorized" });
}
//Remove the old refresh token from the client
res.clearCookie("jwt", { httpOnly: true, secure: false, sameSite: "none" });
//Check if the user exists
const user = await User.findOne({ refreshToken }).select("-password").exec();
//If the user does not exist then the refreshToken does not exist anymore
// Check to which user the refreshToken (Refresh token container user ID) and delete all refresh tokens for that user
if (!user) {
jwt.verify(
refreshToken,
process.env.REFRESH_TOKEN_SECRET,
async (err, user) => {
//Return Forbidden when token is invalid
console.log("hoi");
if (err) return res.status(403).json({ message: "Forbidden" });
//Delete all refresh tokens for that user if token is valid (Maybe warn the user?)
const hackedUser = await User.findById(decoded.userId);
hackedUser.refreshToken = [];
await hackedUser.save();
}
);
return res.sendStatus(403);
}
//Make sure the new refreshtoken is not the same as the old one
const newRefreshTokenArray = user.refreshToken.filter(
(rt) => rt !== refreshToken
);
jwt.verify(
refreshToken,
process.env.REFRESH_TOKEN_SECRET,
asyncHandler(async (err, decoded) => {
if (err) {
user.refreshToken = [...newRefreshTokenArray];
const result = await user.save();
}
if (err || user.email !== decoded.email)
return res.status(403).json({ msg: "Forbidden" }); // Forbidden
//Refresh token was still valid
const roles = Object.values(user.roles);
//Create a new accessToken and refreshToken and return to the user
const accessToken = jwt.sign(
{ userInfo: { userId: user.id, roles: roles } },
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: "30s",
}
);
const newRefreshToken = jwt.sign(
{ userId: user.id, email: user.email },
process.env.REFRESH_TOKEN_SECRET,
{
expiresIn: "1d",
}
);
//Save the new refresh token to the user
user.refreshToken = [...newRefreshTokenArray, newRefreshToken];
const result = await user.save();
res.cookie("jwt", newRefreshToken, {
httpOnly: true,
secure: false,
sameSite: "none",
maxAge: 24 * 60 * 60 * 1000,
});
res.json({ accessToken });
})
);
};
The Refresh call from the Front-End (Not complete since I was trying to debug and just make a simple call)
const useAxios = axios.create({
baseURL: "http://localhost:5000/api/users",
withCredentials: true,
crossDomain: true,
});
const refresh = async () => {
try {
const { data } = await useAxios.get("/refresh");
console.log(data);
} catch (error) {
console.log(error);
}
};
What can I try to solve this issue?
Latest request made to the back-end:
The response that gets sent by the signin endpoint
Thanks to #jub0bs I was able to solve this problem.
I was not correctly setting the cookies because when signin in my axios post call did not have the qithCredentials parameter. I was in the misunderstanding that the withCredentials was only used for sending credentials and not for receiving.
Solution:
Add the axios parameter { withCredentials: true } to post requests.
const signin = async (formValues) => {
const { data } = await axios.post(
"http://localhost:5000/api/users/signin",
{ ...formValues },
{ withCredentials: true }
);
};

cloud functions express cookie __session

I'm trying to create an authentification system using firebase, cloud function and express
I've follow this guide for my express app mixed with the Google Docs about cookies.
Here's my server code
index.ts
import { cors } from "./middlewares/cors";
import { initFirebase } from "./utils/firebase";
import { IRoutes } from "./interfaces";
import { routes } from "./routes";
import * as cookieParser from "cookie-parser";
import * as bodyParser from "body-parser";
import * as Express from "express";
import * as functions from "firebase-functions";
// firebase initialize
initFirebase();
// REST API routes
routes.forEach((routerObj: IRoutes) => {
const app = Express();
app.use(cookieParser());
app.use(cors);
app.use(bodyParser.json());
app.use(
bodyParser.urlencoded({
extended: true
})
);
// export routes individually for cloud functions
app.use(routerObj.router);
exports[routerObj.name] = functions.region("europe-west1").https.onRequest(app);
});
cors.ts
import * as Cors from "cors";
const options: Cors.CorsOptions = {
credentials: true,
methods: "GET,OPTIONS,POST,DELETE,HEAD,PATCH",
preflightContinue: false,
origin: "*"
};
export const cors = Cors(options);
my api route for login
router.post("/login", async (req: Request, res: Response) => {
const { idToken } = JSON.parse(req.body).data;
// // Guard against CSRF attacks.
// if (csrfToken !== req.cookies.csrfToken) {
// res.status(401).send("UNAUTHORIZED REQUEST!");
// return;
// }
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
try {
const sessionCookie = await admin.auth().createSessionCookie(idToken, { expiresIn });
const options: CookieOptions = {
signed: false,
maxAge: expiresIn,
httpOnly: false,
secure: false
};
res.setHeader("Cache-Control", "private");
res.cookie("__session", sessionCookie, options);
res.status(200).send({ cookies: req.cookies });
} catch (e) {
console.error(e);
res.status(401).send("UNAUTHORIZED REQUEST!");
}
});
my api route to check connexion status
router.post("/status", async (req: Request, res: Response) => {
const sessionCookie = req.cookies.__session || "";
try {
const decodedClaims = await admin.auth().verifySessionCookie(sessionCookie!, true);
console.log("decodedClaims: ", decodedClaims);
res.end(JSON.stringify({ data: { decodedClaims } }));
// res.redirect("/profile");
} catch (e) {
console.error(e);
res.status(401).send("UNAUTHORIZED REQUEST!");
}
and then how I call the api from my client side (http://localhost:3001)
const idToken = await user.getIdToken();
try {
await fetch("http://localhost:5003/test/europe-west1/user/login",
{
method: "POST",
headers: {
ContentType: "application/json",
Accept: "application/json"
},
body: JSON.stringify({
data: {
idToken
}
})
}
);
} catch (e) {
console.error(e);
}
First point, no __session cookie's created with this code. However, the response of the request is
But nothing in the cookies section of the browser and when I try to get it with req.cookies.__session
Nevertheless if I try to fetch http://localhost:5003/test/europe-west1/user/login directly from the same origin, everything work
I suggest the problem come from cross-origin authorizations
I've checked a lot a issues about this
firebase cloud function won't store cookie named other than "__session"
Express - Firebase - Check undefined req.cookie.__session without throwing error
And more, but nothing work

How can I solve the Access-Control-Allow-Origin CORS error in my MERN app for MSAL auth?

I'm trying to authenticate through MSAL in my MERN app by clicking a button.
However I get this error :
Access to XMLHttpRequest at
'https://login.microsoftonline.com/common/oauth2/v2.0/a...'
(redirected from 'http://<SERVER_URL>/api/auth/signin') from origin
'http://<CLIENT_URL>' has been blocked by CORS policy: No
'Access-Control-Allow-Origin' header is present on the requested
resource.
Here is the code of my NodeJS server :
const express = require("express");
const session = require('express-session');
const authRoutes = require("./routes/auth.routes");
const msal = require('#azure/msal-node');
const cors = require("cors");
require("dotenv").config();
const app = express();
const corsOptions = {
origin : process.env.CLIENT_URL,
credentials: true,
"allowedHeaders": ["sessionId", "Content-Type"],
"exposedHeaders": ["sessionId"],
"methods": "GET,HEAD,PUT,PATCH,POST,DELETE",
"preflightContinue": false
}
app.use(cors(corsOptions));
// Demo only
app.locals.users = {};
// MSAL Config
const msalConfig = {
auth: {
clientId: process.env.OAUTH_APP_ID,
authority: process.env.OAUTH_AUTHORITY,
clientSecret: process.env.OAUTH_APP_SECRET
},
system: {
loggerOptions: {
loggerCallback(loglevel, message, containsPii) {
console.log(message);
},
piiLoggingEnabled: false,
logLevel: msal.LogLevel.Verbose,
}
}
};
app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);
// Session middleware
app.use(session({
secret: 'your_secret_value_here',
resave: false,
saveUninitialized: false,
unset: 'destroy'
}));
app.use("/api/auth", authRoutes);
app.get("/", (req, res) => {
res.send("Hello World!");
});
app.listen(process.env.PORT, () => {
console.log(`Server is running on port ${process.env.PORT}`);
});
module.exports = app;
Here are my auth.controller methods :
module.exports = {
signIn: async (req, res) => {
const urlParameters = {
scopes: process.env.OAUTH_SCOPES.split(','),
redirectUri: process.env.OAUTH_REDIRECT_URI
};
try {
const authUrl = await req.app.locals.msalClient.getAuthCodeUrl(urlParameters);
res.redirect(authUrl);
} catch (error) {
console.log(`Error: ${error}`);
res.redirect("/");
}
},
callback: async (req, res) => {
const tokenRequest = {
code: req.query.code,
scopes: process.env.OAUTH_SCOPES.split(","),
redirectUri: process.env.OAUTH_REDIRECT_URI
};
try {
const response = await req.app.locals.msalClient.acquireTokenByCode(tokenRequest);
req.session.userId = response.account.homeAccountId;
const user = await graph.getUserDetails(response.accessToken);
req.app.locals.users[req.session.userId] = {
displayName: user.displayName,
email: user.mail || user.userPrincipalName,
timeZone: user.mailboxSettings.timeZone
};
} catch (error) {
console.log(`Error: ${error}`);
}
res.redirect("/");
},
signOut: async (req, res) => {
if (req.session.userId) {
const accounts = await req.app.locals.msalClient.getTokenCache().getAllAccounts();
const userAccount = accounts.find(a => a.homeAccountId === req.session.userId);
if (userAccount) {
req.app.locals.msalClient.getTokenCache().removeAccount(userAccount);
}
}
req.session.destroy(err => res.redirect("/"));
}
};
And here is the React part :
import React from 'react';
import axios from "axios";
const App = () => {
const handleConnect = () => {
axios({
method: "get",
url: `${process.env.SERVER_URL}/api/auth/signin`,
withCredentials: true
})
.then(res => console.log(res.data))
.catch(err => console.log(err));
};
return (
<button onClick={handleConnect}>Connect</button>
);
};
export default App;
In my Azure Active Directory admin center, my redirection URIs are :
"<CLIENT_URL>" as "SPA"
"<SERVER_URL>/api/auth/signin" as "Web"
The Network tab in devtools helps troubleshoot this sort of thing.
You probably need to handle CORS preflight requests, by putting something like this in your express app to handle OPTIONS requests.
app.options('*',cors())
Put this line before app.use() for any routes.
This one bit me in production. Ouch!
Setting Access-Control-Allow-Origin to * is very risky and not recommended. It means that you are allowing any origin to receive a response back from your server.
removing CORS means that Same Origin Policy will be enforced, therefor it won't work.
To solve the issue between your client and server, what you can do is set a proxy in your package.json file of the React app, which will point to your server: "proxy": "YourServerURI".
Regarding the initial question of the error from MSAL, I would suggest to double check that your app is registered correctly and has the permission to access your server.

Nodejs Express + Nuxt - Session Cookies

I am using an Express App for the backend and VueJs with Nuxt (Server Side Rendering). My problem is that the cookies are not getting saved when the session is getting refreshed.
Server:
const express = require('express')
const cookieParser = require('cookie-parser')
const { loadNuxt } = require('nuxt')
const app = express()
app.use(cookieParser())
// Middleware
app.use(async (req, res, next) => {
// ...
if (sessionExpired && refreshTokenIsValid) {
// Generate new session
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
return next()
}
})
...
Login route
router.get('/login', async (req, res, next) => {
// ...
res.cookie('sessionToken', token, { maxAge: 86400000, path: '/' })
res.cookie('sessionId', id, { maxAge: 86400000, path: '/' })
res.cookie('refreshToken', refreshToken, { maxAge: 86400000, path: '/' })
res.status(200).redirect('/')
})
Client:
async asyncData({ $axios }) {
const data = await $axios.get('/something')
},
methods: {
async someMethod() {
let data = await this.$axios.$get('/something')
}
}
The cookies are not getting saved when sending a request from asyncData().
I solved the problem by using an axios helper.
Solution source: proxy cookies
// plugins/ssr-cookie-proxy.js
import { parse as parseCookie } from 'cookie';
function parseSetCookies(cookies) {
return cookies
.map(cookie => cookie.split(';')[0])
.reduce((obj, cookie) => ({
...obj,
...parseCookie(cookie),
}), {});
}
function serializeCookies(cookies) {
return Object
.entries(cookies)
.map(([name, value]) => `${name}=${encodeURIComponent(value)}`)
.join('; ');
}
function mergeSetCookies(oldCookies, newCookies) {
const cookies = new Map();
function add(setCookie) {
const cookie = setCookie.split(';')[0];
const name = Object.keys(parseCookie(cookie))[0];
cookies.set(name, cookie);
}
oldCookies.forEach(add);
newCookies.forEach(add);
return [...cookies.values()];
}
export default function ({ $axios, res }) {
$axios.onResponse((response) => {
const setCookies = response.headers['set-cookie'];
if (setCookies) {
// Combine the cookies set on axios with the new cookies and serialize them
const cookie = serializeCookies({
...parseCookie($axios.defaults.headers.common.cookie),
...parseSetCookies(setCookies),
});
$axios.defaults.headers.common.cookie = cookie; // eslint-disable-line no-param-reassign
// If the res already has a Set-Cookie header it should be merged
if (res.getHeader('Set-Cookie')) {
const newCookies = mergeSetCookies(
res.getHeader('Set-Cookie'),
setCookies,
);
res.setHeader('Set-Cookie', newCookies);
} else {
res.setHeader('Set-Cookie', setCookies);
}
}
});
}

Resources