cloud functions express cookie __session - node.js

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

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.

Why does Heroku Node/Express site give CORS header error on all computers except development machine?

I'm trying to get a test React frontend and Node/Express backend to correctly set session cookies at Heroku.
This frontend/backend work locally.
This frontend/backend work at Heroku in every browser on my development machine.
But on every other machine I have tested (Windows, Ubuntu), I get this Cross-Origin-Request-Blocked error:
Here is the backend code, where I configure CORS origin correctly:
import express from 'express'
import cors from 'cors'
import morgan from 'morgan'
import session from 'express-session';
import dotenv from 'dotenv';
import cookieParser from 'cookie-parser';
dotenv.config();
const app = express();
app.use(morgan("dev"));
app.set('trust proxy', 1);
app.use(cors({
origin: process.env.FRONTEND_ORIGIN,
credentials: true
}));
app.use(cookieParser());
app.use(session({
name: 'testsession',
secret: 'h$lYS$cr§t!',
resave: true,
saveUninitialized: true,
cookie: {
httpOnly: true,
maxAge: 60 * 60 * 24,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production"
}
}))
app.get('/', (req, res) => {
let user = req.session.user;
if (!user) {
res.json({ message: `${(new Date()).toISOString()}: nobody is logged in` })
} else {
res.json({ message: `${(new Date()).toISOString()}: ${user} is logged in` })
}
});
app.get('/login', (req, res) => {
req.session.user = "user001"
res.json({
message: `${(new Date()).toISOString()}: ${req.session.user} is now logged in`
})
})
app.get('/logout', (req, res) => {
req.session.destroy();
res.json({ message: `${(new Date()).toISOString()}: user logged out` })
});
const PORT = process.env.PORT || 3011
app.listen(PORT, () => {
console.log(`API listening on http://localhost:${PORT}`);
});
Here is my frontend code:
import { useState } from 'react';
import './App.scss';
function App() {
const [message, setMessage] = useState('click a button');
const backendUrl = process.env.REACT_APP_BACKEND_URL;
const handle_checkuser = async () => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
const response = await fetch(backendUrl, requestOptions);
const data = await response.json();
setMessage(data.message);
}
const handle_login = async () => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
const response = await fetch(`${backendUrl}/login`, requestOptions);
const data = await response.json();
setMessage(data.message);
}
const handle_logout = async () => {
const requestOptions = {
method: 'GET',
credentials: 'include'
};
const response = await fetch(`${backendUrl}/logout`, requestOptions);
const data = await response.json();
setMessage(data.message);
}
return (
<div className="App">
<div><button onClick={handle_checkuser}>checkuser</button></div>
<div><button onClick={handle_login}>login</button></div>
<div><button onClick={handle_logout}>logout</button></div>
<div>{message}</div>
</div>
);
}
export default App;
Why would it be getting this CORS error with some machines and not others?
Backend:
https://github.com/edwardtanguay/et-cookietest-backend
https://et-cookietest-backend.herokuapp.com
Frontend:
https://github.com/edwardtanguay/et-cookietest-frontend
https://et-cookietest-frontend.herokuapp.com
ADDENDUM
I also noticed that on every other machine except for my development machine, the HTTP connection is not secure. This seems to be the cause of the cookie-setting problem.
But how can that be? Why would one particular computer receive HTTPS connections from a website and others HTTP?

Invalid CSRF Token in React but valid in Postman

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

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