Why won't my React app send HTTP-only cookies in WebSocket upgrade requests in production? - reactjs

I'm currently building a full-stack TypeScript live chat app with React + Vite on the frontend and Node on the backend. I have two separate servers running: one is a REST API and OAuth2 auth server built with Express and Passport.js and the other one is a WebSockets server built with the ws package. They run independently (no interprocess communication whatsoever) and use stateless auth in the form of JWTs.
Here's how my current flow works: users first log in with either their Google or GitHub account, and once the first server has verified their identity, it sends an HTTP-only cookie down to the client. This cookie is send back to the server on all subsequent requests and I have some middleware that runs on the REST API to parse and verify the JWTs on protected routes. Once it has the cookie, the client then initiates a WS connection with the second server, which also checks for the JWT cookie in the incoming HTTP Upgrade request and verifies its signature before allowing the new client to continue exchanging messages:
import { WebSocket, WebSocketServer } from "ws";
import { baseDataSchema } from "./zod/schemas";
import prisma from "./prisma";
import { asyncJWTverify } from "./misc/jwt";
import { UserJwtReceived } from "../types/jwt";
import { handleJoinGroup } from "./websockets-handlers/join-group";
// Websockets server setup
const wss = new WebSocketServer({ port: Number(process.env.WS_PORT) });
const userSocketMap = new Map<string, WebSocket>();
wss.on("listening", () => {
console.log(`WebSockets server started on port ${process.env.WS_PORT}`);
});
wss.on("connection", async function connection(ws, req) {
// authenticate incoming websocket connection
const cookies = req.headers.cookie;
if (!cookies) return ws.close();
let currentUser: UserJwtReceived = { id: "", iat: 0 };
try {
// Decode auth JWT
const token = cookies.split("=")[1];
currentUser = (await asyncJWTverify(
token,
process.env.JWT_SECRET as string
)) as UserJwtReceived;
} catch (err) {
console.error(err);
return ws.close();
}
// check for JWT expiry
const expiryTime = Number(process.env.JWT_EXPIRY);
if (Math.round(Date.now() / 1000) - currentUser.iat > expiryTime) {
return ws.close();
}
// Bind user ID to WebSocket, add it to map
// TypeScript doesn't complain about this because I've extended ws's WebSocket interface
ws.userId = currentUser.id;
userSocketMap.set(currentUser.id, ws);
console.log(`User ID ${ws.userId} connected`);
ws.on("message", async function message(rawData) => {
// ... actual app logic goes here
})
ws.on("close", function () {
if (!ws.userId) return;
console.log(`User ID ${ws.userId} has disconnected`);
userSocketMap.delete(ws.userId);
});
})
Both servers and the React frontend app run on different URLs, both on local dev and prod, so all requests are cross-origin, but CORS is enabled on the REST API/auth server and as far as I know the WebSockets protocol doesn't implement any CORS policies...
The problem I'm currently facing is that in my local dev environment, the cookie that contains the JWT is sent along with Upgrade request no problem, but after deploying my app to AWS Lightsail (it's a VPS service similar to EC2) and setting up NGINX, my React frontend is no longer able to include the cookie with the upgrade request.
After spending literally the whole day debugging, I've been able to rule out a faulty NGINX config as the root of the problem, since I can use wscat to connect (and most importantly, successfully authenticate) to my production WS server by manually including the Cookie header.
I still have no idea why my React app won't properly send the HTTP-only auth cookie to my WS server. Does anyone have any clue as to why this is happening?
I expected the HTTP-only cookie containing the JWT to be sent along with the HTTP Upgrade request, just like I've been able to do in my local dev environment, but no luck.

Related

Heroku: Django backend is not working, getting error GET http://localhost:8000/api/todos/ net::ERR_CONNECTION_REFUSED

I actually created a fullstack todo app with django as backend and react as frontend. The frontend is working perfectly fine, you can that here -> https://ym-todo-application.herokuapp.com. But somehow my application cannot connect to the django backend, also on inspecting on browser i saw this error -> GET http://localhost:8000/api/todos/ net::ERR_CONNECTION_REFUSED.
Containing lot of files and code so i pushed them on bitbucket to make it easier to debug. here's the link https://bitbucket.org/Yash-Marmat/todo-app-fullstack/src/master/.
Thanks in advance.
You need to change the REST API server address. The localhost:8000 is the server address used in the development.
What is more, I see in your code that each time you write the request you have hard-coded server URL. You don't need to do this. You can set the server address by setting baseURL:
import axios from "axios";
if (window.location.origin === "http://localhost:3000") {
axios.defaults.baseURL = "http://127.0.0.1:8000"; // development address
} else {
axios.defaults.baseURL = window.location.origin; // production address
}
Then in the request, you only write the endpoint address, example:
axios.put(`/api/todos/${item.id}/`, item)
Please see my article Docker-Compose for Django and React with Nginx reverse-proxy and Let's Encrypt certificate for more details about Django and React deployment.

React App + Spring Boot - JWT auth token inside a cookie is not set in Chrome

I'm trying to configure Spring Boot to set-cookie containing JWT auth token following a sign-in request from my react app, and then the expectation is that the browser will automatically set this cookie to all requests as specified by the cookie path. The behaviour is ok on my friend's environment - same code, Chrome browser, different machine. I tried clearing node_modules, mvn clean install, also tried different browsers Chrome and FireFox, no success.
Here is the all the relevant code (let me know if I'm missing something else important)
React is running on localhost:3000
Spring Boot is running on localhost:8080
There is a proxy in the package.json
"proxy": "http://localhost:8080",
To test the auth flow we are issuing a sign-in request from the sign-in form (react), the request is successfully proxied to port 8080 and the response from the server is successfully returning the JWT token as part of an auth cookie. The cookie is specified to the /api path. Network request as seen in Chrome below:
Immediately after the login, the react app is issuing a second HTTP request to the back-end, but a break-point on the server shows no cookies are passed from the browser as part of this request. The request is to http://localhost:3000/api/user.
In the front-end we are using fetch to make that request and it looks like this:
fetch("/api/user, {
credentials: "same-origin"
})
Just for additional context this is how we return the original cookie from the server, upon a successful login:
#PostMapping("/signin")
public ResponseEntity signin(#RequestBody AuthenticationRequest data, HttpServletResponse response) {
try {
String username = data.getUsername();
Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, data.getPassword()));
User user = (User) authentication.getPrincipal();
String token = jwtTokenProvider.createToken(user);
final Cookie cookie = new Cookie("auth", token);
cookie.setSecure(!environment.acceptsProfiles(Profiles.of("dev")));
cookie.setHttpOnly(true);
cookie.setMaxAge(Integer.MAX_VALUE);
cookie.setPath("/api");
response.addCookie(cookie);
return ok(buildUserResponseObject(user));
} catch (AuthenticationException e) {
throw new BadCredentialsException("Invalid username/password supplied");
}
}
Is there anything wrong with our approach? What could be preventing my browser from passing on the auth cookie?
Oh this is embarrassing...
The issue was this line
cookie.setSecure(!environment.acceptsProfiles(Profiles.of("dev")));
!environment.acceptsProfiles(Profiles.of("dev")) was evaluating to true and it was causing the cookie to be only passed if the connection is secure, which it wasn't because it was localhost. Mystery solved.

How Do I Configure REST Urls When Running React With Azure API-Management?

I have a reactjs front end that gets data from a spring boot backend via rest calls.
Running locally the code for this looks like:
axios.defaults.baseURL = 'http://localhost:8080/api';
axios.get('/devices').then((resp) => {
this.setState({devices: resp.data});
}).catch(() => {
console.log('Failed to retrieve device details');
});
When I build the code to deploy with npm run build I still have localhost as the url.
How do I build it so that developing locally it uses localhost but deploying it uses a different url?
Once I am in Azure I will have one front end and multiple back ends that need to be pointed to depending on who is logged in.
How do I configure the API-Management layer to route the calls to the correct back end depending on who is logged in (using AD for auth)?
Since I am using APIM for the routing, what should the baseURL be?
Manage those variables in a config file and load based on the environment.
Local Values
you can hardcode local variable directly in a config file
Production Values
- keep place holders and set them from build pipeline or
- hard code them also
E.g:
config.js
const serverVars = {
authUrl: '#{authUrl}#',
apiUrl: '#{apiUrl}#',
};
const localVars = {
authUrl: 'local_auth_url',
apiUrl: 'local_api_url',
};
export function getConfiguration() {
if (process.env.NODE_ENV === 'production') {
return serverVars;
}
return localVars;
}
when you call apiUrl
import axios from 'axios';
import { getConfiguration } from 'config';
axios.defaults.baseURL = getConfiguration().apiUrl;
One approach can be using environment variable. You can have different url based on the environment variable.
Another way is just removing the domain from base url, but this will work only when your backend and frontend domain & port are identical.

Express JS how to redirect all http traffic to https (Heroku)

I want a mechanism in my Express JS app to redirect all http traffic to https traffic, similar to Rails's force_ssl config. (In Rails's case, it simply sends back a Redirect response to the client
I think the Express server code would go in Server.js, but I need to distinguish these requests from secure (https) requests that should go to the real app.
I think the redirection code looks like this:
var http = express.createServer();
http.get('*', function(req, res) {
res.redirect('https://' + req.headers.host + req.url);
})
// have it listen on 8080
http.listen(80);
You'll note that I can't actually listen for the port by port number, because on Heroku there's the added complication that the app may be redeployed listening on app port of their choosing (it changes every time is redeployed).
so, in essence, I need a way to detect the protocol (http or https) without using the port number here in the Express setup, and then redirect http traffic to https.
The app itself is modeled after "Create React App on Heroku" here https://originmaster.com/running-create-react-app-and-express-crae-on-heroku-c39a39fe7851, so essentially it is a REACT app being served by Express JS on Heroku.
and with example app here https://github.com/Johnnycon/crae-heroku
where I'm stuck is that i've examined the 'process' variable, and while it contains a lot of information, it does not seem to contain any request information, like the protocol or url, as the request comes in.
Any tips or suggestions?
Typically in your server.js near the top, add this function:
// On Heroku, SSL termination happens at the load balancer,
// BEFORE encrypted traffic reaches your node app.
function enforceHttps(req, res, next) {
// Check if directly requested via https
if (req.secure) {
next();
// Heroku sets a header X-Forwarded-Proto to pass the user requested protocol
} else if ((req.headers['x-forwarded-proto'] || '').substring(0, 5) === 'https') {
next();
// Only redirect GET and HEAD requests
} else if (req.method === 'GET' || req.method === 'HEAD') {
const host = req.headers['x-forwarded-host'] || req.headers.host;
// redirect with 301 Moved Permanently instead of default 302
res.redirect(301, `https://${host}${req.originalUrl}`);
} else {
res.status(403).send('This server requires an HTTPS connection.');
}
}
and then later in the file after you define you app, typically as const app = express();:
app.use(enforceHttps);

After deploying React/Express app to Heroku unable to start passport.js flow (page reloads instead) [duplicate]

I'm building a node + express server, with create-react-app to the frontend.
I used passportjs for auth routes handling, and all the stuff totally working on localhost ( backend on port 5000 and frontend on port 3000, with a proxy ).
When I deploy to Heroku, seems like the server can't recognize my auth routes and so heroku serve up static index.html.
If I test my APIs with Postman all seems to work ( I can see the html page for google oauth ), but with an anchor tag in my react app or manually writing the endpoint in the url, I can see only the same page reloading.
My server index.js:
const express = require('express')
const passport = require('passport')
const mongoose = require('mongoose')
const path = require('path')
// KEYS
const keys = require('./config/keys')
// MONGOOSE MODELS
require('./models/User')
mongoose.connect(keys.mongoURI)
// PASSPORT SETUP
require('./config/passport')
// CREATE THE SERVER
const app = express()
// EXTERNAL MIDDLEWARES
require('./middlewares/external')(app)
// ROUTE HANDLERS
require('./routes/authRoutes')(app)
// PRODUCTION SETUP
if (process.env.NODE_ENV === 'production') {
// express serve up production assets ( main.js, main.css )
app.use(express.static('client/build'))
app.get('*', (req, res) => {
res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'))
})
}
// START THE SERVER
const PORT = process.env.PORT || 5000
app.listen(PORT)
Flow:
LOCALHOST:
react app starts -> I click 'Google Login' -> GET request to "/auth/google" -> google oauth flow -> redirect to "/" and my react app reappears, the user is logged in.
HEROKU:
react app on my-app.herokuapp.com/ -> click on "Google Login" -> the page reloads, nothing happens. the user is not logged in.
Please guys, help me.
Thanks
This is a result of the service worker being installed by default to make your app a Progressive Web App
To determine if this is an issue for you, test your heroku production mode app in incognito mode. The request for /auth/google should now reach the server and behave as it does in development.
Once you determine it is an issue, you can remove the
import registerServiceWorker from "./registerServiceWorker";
from your /client/src/index.js file.
You browser cache may already contain an installed service worker so you may have to
clear browser cache on a user browsers
uninstall the server worker programmatically
import { unregister } from './registerServiceWorker';
....
unregister();
I had the same issues with same symptoms exactly.
For me the cause was a typo in the keys: in server/config/prod.js I had a line reading cookieKey: process.env.COOKIE_KEY but in Heroku Config Variables that variable was named cookieKey. Renaming it to COOKIE_KEY inside Heroku solved the issue.
If you've followed the Stephen Grider tutorial one thing I'm wondering: Is your passport.js file in config or services? I see you've written in index.js: require('./config/passport')
whereas mine in index.js is require('./services/passport')
may not be your solution to the google oauth flow hanging in production but may help.

Resources