I have an api (nodejs + express) running on azure webapp service and a frontend (nuxtjs) running locally and on cloudlfare. My auth flow uses passportjs LocalStrategy and worked fine when I developed it and ran the api locally. Now that I have deployed the api on azure app service, my front end always gets a 401 not authorized response. I am pretty sure it is not an issue with the frontend nuxt app since the problem occurs only when trying to use the azure hosted api.
I am using express-session with a postgres database to store session information.
const sessionPool = new Pool() //if this becomes problematic consider sessionPool.end() in logout
auth.use(session({
resave: false,
saveUninitialized: true,
secret: process.env.SESSION_SECRET,
cookie: {maxAge: 1000 * 60 * 60 * 24}, //one day
store: new (require('connect-pg-simple')(session))({
SameSite: 'none',
pool: sessionPool,
tableName: 'session'
}),
}))
Everything seems to work right at first. The user credentials get sent to the backend, they are run against the database and if they match an existing user it creates a session and CLAIMS to send the user info in the response. (some of) the cookies exist on the front end, but it seems like some are missing. When running the application locally the front end
stores 5 cookies but in production it only seems to store 3. All api calls that require authorization return 401 not authorized even though the client seems to have the right information and the backend shows they have a live session (I can see the session data in the db table).
//req.isAuthenticated() always returns false on the azure web app, but true when run locally
auth.get("/user", async (req, res) => {
try {
if (req.isAuthenticated()) {
res.json({ user: req.user });
} else {
console.log("User not authenticated");
res.sendStatus(401);
}
} catch (err) {
console.log(err);
res.sendStatus(500);
}
});
I believe it is an issue with the azure app service blocking my authorization flow. The app service is using the node 16 run time and windows os (so it's using iisnode). Anyone have any insight?
Azure app service is a reverse proxy, meaning client details aren't going to be as expected from a typical request. For example, the client IP address is available from the x-forwarded-for header instead.
You need to tell express that your app is running behind a proxy:
app.set("trust proxy", 1);
and at the same time, you should explicitly define the cookie domain and make sure httpOnly is enabled, to help prevent session theft from XSS attacks.
cookie: {
domain: 'my.website.com',
httpOnly: true,
maxAge: 24*3600000, // 24 hours
}
Learn more about running express behind a proxy.
Related
I created a frontend application using React which I then deployed on Netlify. The frontend app interacts with my express backend application which I created and deployed to Render. Any interaction between the frontend and backend is protected by authentication using passportjs. The backend app uses passports local strategy to verify with a mongoDB connection if the user exists/username and password are correct. Once confirmed, I use passports serialize and deserialize functions to maintain a login session.
During development I worked through this application on http://localhost. During this time, everything worked as intended. The login session was saved as a cookie and I was able to send that through the frontend app as part of future requests to the backend app to authenticate the user.
When I deployed my frontend app to Netlify and backend app to Render, the session cookie no longer saved meaning the user could never be authenticated in the backend as every request, after logging in, had no saved session cookie to send along to the backend.
A little more detail, for the express-session, used in combination with passport to create the login session, I used connect-mongo to store the session on a MongoDB. Each time I login through the frontend, I can see the session being created and stored in MongoDB. The problem appears to be that the cookie is not being transferred over in the response from the backend and I am not sure why.
Here is the session options I currently have in my express backend app:
app.use(
session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({ mongoUrl: process.env.MONGODB_URI }),
cookie: {
secure: true,
sameSite: "none",
httpOnly: true,
expires: 24 * 60*60*1000
}
})
);
I have tried searching around to find a solution and have messed with many of the options including changing secure, changing httpOnly, setting sameSite to false and none, adding a domain which points to the frontend URL and another for the backend URL, removing any security packages that I installed such as helmet and a rate limiter and nothing seems to be working.
I have also set the access control header for credentials to true:
app.use((req, res, next) => {
res.setHeader("Access-Control-Allow-Origin", process.env.DOMAIN_URL);
res.setHeader("Access-Control-Allow-Credentials", true);
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
next();
});
For all my requests I am using fetch. I can across a similar problem to mine but it was not answered: Browser not saving session cookie from cross domain server
Any help or suggestions would be much appreciated!
Solved it. The issue was with trust proxy. Since I am running multiple domain servers, the backend was behind a reverse proxy. By enabling trust proxy, my backend app will understand that its sitting behind a proxy and the X-Forwarded- header fields may be trusted. To enable it I added the following code in my backend app:
app.set("trust proxy", 1);
I used google authentication in my MERN stack app using passport js. I have deployed frontend to vercel and backend to render. in my localhost, I can log in successfully and it returns cookies to my frontend from the backend. but in the deployed version when I try to log in it does log in but it doesn't return any cookies to the frontend. I did some research and found that cookies can't be shared between different domains. my question is should I host my backend in a subdomain and frontend in the main domain?
frontend: https://passport-frontend-seven.vercel.app/
backend : https://popupchat-backend.onrender.com/
// how i used express-session:
app.use(
session({
secret: "secretcode",
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true,
sameSite: "none",
secure: true,
domain: process.env.CLIENT_URL,
maxAge: 1000 * 60 * 60 * 24 * 7, // One Week
},
})
);
app.set("trust proxy", 1);
Yes, you are correct in your understanding that cookies can't be shared between different domains. If you have your frontend and backend deployed on different domains, then you will have difficulty accessing the cookies set by the backend in the frontend.
Hosting your backend on a subdomain of your main domain is one solution to this problem. For example, if your main domain is example.com, you can host your backend at api.example.com. This way, both the frontend and the backend will be under the same main domain and you will be able to share cookies between them.
However, there are other solutions as well, such as using a token-based authentication system, where the backend returns a token after a successful login, and the frontend can store this token in local storage or a cookie. The frontend can then send this token in the header of subsequent API requests to authenticate the user on the backend. This approach can be more secure than using cookies, since cookies are often stored in plain text and can be easily compromised.
I'm struggling with understanding cookie credentials in detail.
The main issue is that my frontend and backend run on different domains. On localhost, they have different ports (localhost:4200 and localhost:3000), and on my server different subdomain (example.com and api.example.com).
Basic setup:
Angular send post request to login:
this.http.post(environment.apiUrl + 'users', credentials)
NodeJS validates the request and creates an express-session (session in DB and cookie with sessionId):
app.use(session({
secret: process.env.SECRET,
saveUninitialized: false,
store: mongoDBStore,
name: 'node_session',
cookie: {
secure: true,
httpOnly: true,
maxAge: 5 * 60 * 1000
}
}));
app.post('/', (req, res) => {
// some validations
req.session.userId = result._id;
res.send(result._id);
}
My understanding
From my understanding cookies get stored for the server domain. So if both run on the same domain, on each request, the backend can use the session cookie. But with different domains, the session is empty on further requests.
Additionally, I found the cookie node_session in the frontend and backend. So if I add { withCredentials: true } to the requests of Angular, everything works. But in this case, I use the cookie from the frontend.
My Questions:
Is it secure to use the frontend cookie?
Why can I find the session cookie in the frontend and backend?
Why is my backend unable to find the session cookie?
session information is stored on the backend. frontend just stores uniq session identifier in cookies. If you want to understand somehow that requests came from the same user one of the options is to use {withCredentials: true}, without it cookies are just not saved to the client, and backend can not define, whether different queries were send by one client or not.
as an alternative to cookies there is a popular option to use authorization token, but it is a bit more difficult to setup
i'm working on micro services with express js with express-session set up like this:
server.use(session({
name: 'apps',
resave: false,
saveUninitialized: false,
secret: 'secretToken',
cookie: {
sameSite: false,
secure: false, // true
httpOnly: false, // false
}
}));
i apply the same setup with all services.
but then it's only work with just only one services. other services can't share the same data from other service, i'm putting access token in the req.session to let other apps having the same access token.
Is there a way for me to let different service using the same session?
The default server-side session storage is MemoryStore. Which means each service store the session data on their own process. Memory cannot be shared between different processes.
In order for microservices to share session data, we need separate storage, such as connect-redis, connect-mongo.
All microservices connect the redis or database server and use them as session data storage when initializing express-session middleware.
Workflow:
Client send HTTP request(with session id stored in a cookie) => Service A or Service B or Service C => express-session get the session data from redis or database by session-id => do your things.
So we have a "web" server and an API server - the web server serves up HTML and the API server serves up JSON.
We use Passport's Twitter strategy to authenticate on the web server.
My question is - what is the best way to check on the API server that the user who has authenticated with the web server is also authenticated with the API?
My assumption is that we should put most of the Passport code into the web server, have the user authenticate with it as usual with Passport, and use some middleware in the API server like so to check if the user is logged in (has a session):
app.use(passport.initialize());
app.use(passport.session());
app.use(expressSession({
secret: 'keyboard cat',
store: new MongoStore({mongooseConnection: mongoose.connection})
}));
app.use(function(req,res,next){
if(req.isAuthenticated()){
next();
}
else{
console.log('unauthenticated request');
res.status(403).json({error:'unauthorized request'});
}
});
however, the above doesn't seem to be enough. I am utterly confused about exactly what code I need on the API server - I believe I need to read from the same session store that the web server writes to and to look at a token in the request to the API server and compare it with a token in the session store?
I am not sure I understand what the req.isAuthenticated() function does? It seems like I now need to write my own req.isAuthenaticated() function which reads from the session store asynchronously...
Does anyone have an example of how to do this right?
You might be able to do as you said - authenticate using the web server and just verify that they are authenticated using the API.
Providing that both servers share the same remote session store, and both endpoints have access to the express session cookie, you should be able to implement something like this for the API:
function ensureAuthenticated(req, res, next) {
if (req.isAuthenticated())
return next();
else
// Not authenticated
}
Or
if (req.user) {
// logged in
} else {
// not logged in
}
As middleware (as you showed in your example), or on a per-route basis.
This may not be viable if you can't access the same, shared, session cookie though. Other options may be available depending on what structure your app is built in ie. - if the web server is speaking directly to the API.
You can read the raw req.isAuthenticated function from the passport github.