WRITEUP
Three settings that make sign-in work behind a proxy
LinkLocker runs behind Nginx. That one fact touches the rate limiter, the secure cookie, and the Google callback address. Here is why each setting exists.
FROM THE PROJECT
Google OAuth with passport.js, live on Azure
LinkLocker runs on an Azure server with Nginx in front of it. Nginx handles the secure (HTTPS) connection and passes each request on to my Express app, which listens on port 3000 and is not open to the internet. My teammate set up Nginx, and I wrote the Google sign-in. Putting the two together showed me that this one arrangement quietly touches three things in my code.
The problem: the app only sees Nginx
From Express’s point of view, every request comes from the same visitor: Nginx, at 127.0.0.1. Nginx knows the truth, so on the way through it writes it into headers: X-Real-IP and X-Forwarded-For carry the visitor’s address, and X-Forwarded-Proto says whether the visitor used HTTPS. Express ignores those headers unless it is told to trust them. One line does that:
// Trust loopback traffic so the rate limiter can correctly read the client IP
// when the app is behind local proxying.
app.set("trust proxy", "loopback");"loopback" means it only trusts a proxy running on the same machine, not anything that merely claims to be one.
Place 1: the rate limiter
Creating links is limited to 20 per 15 minutes per visitor, to stop spam.
// Limit how often a client can create short links to reduce spam and abuse.
const linkCreationLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
limit: 20,
standardHeaders: "draft-8",
legacyHeaders: false,
message: { error: "Too many links created. Please try again later." },
});A limit per visitor only works if the app can tell visitors apart. Without the proxy setting, everyone would look like 127.0.0.1, so all visitors would share one budget of 20. It is the same kind of mistake as the shared key in my ThinkBoard limiter, with a different cause.
Place 2: the sign-in cookie
After Google confirms who you are, the app gives your browser a cookie that says “this person is signed in”. In production that cookie is marked secure, which means the browser only sends it over HTTPS. But Express is talking plain HTTP to Nginx, so it has to learn that the visitor’s original connection was HTTPS. That is what X-Forwarded-Proto is for, and why trust proxy matters here too.
cookie: {
httpOnly: true,
sameSite: "lax",
// Secure cookies require HTTPS (nginx terminates TLS and "trust proxy" is set).
secure: process.env.NODE_ENV === "production",
maxAge: 7 * 24 * 60 * 60 * 1000,
},The other settings are protections. httpOnly keeps scripts on the page from reading the cookie, sameSite: "lax" limits when other websites can make a browser send it, and maxAge keeps you signed in for seven days. The session itself is stored in MongoDB and the cookie only refers to it by the user’s id, so logins survive a restart of the server.
Place 3: the address Google sends you back to
When you click Sign in with Google, you go to Google and then back to a callback address on my site. Google only sends people to addresses that were registered ahead of time, and the address has to match exactly, including https and the path. There are two: http://localhost:3000/auth/google/callback for development and the Azure address for production. The app reads which one to use from an environment setting. If they do not match, Google refuses with an error called redirect_uri_mismatch.
Keeping the secrets out of the code
- The Google client ID and secret, the callback address, and the session secret are read from environment settings, never written in the code.
- The
.envfile is not committed. The repository only has.env.examplewith placeholder values. - The server refuses to start if any of them is missing, so a mistake shows up immediately instead of as a strange login failure later.
What I would change
The Google consent screen is still in testing mode, which means only accounts I list as test users can sign in. That is fine for a course project, but it would have to be published before real visitors could use it.
What I took from it
When an app sits behind another program, a few settings have to agree: what the proxy forwards, what the app trusts, and what Google was told. None of them is hard, but each can fail quietly. Writing them down in one place, the Milestone B write-up in the repository, is what made them easy to find again.
Found this useful?
I'm happy to talk through the details, or what I would do differently.
