HTTP cookies are not working for localhost subdomains - node.js

I have a node/express API that will create an HTTP cookie and pass it down to my React app for authentication. The setup was based on Ben Awad's JWT HTTP Cookie tutorial on Youtube if you're familiar with it. Everything works great when I am running the website on my localhost(localhost:4444). The issue I am now running into is that my app now uses subdomains for handling workspaces(similar to how JIRA or Monday.com uses a subdomain to specify a workspace/team). Whenever I run my app on a subdomain, the HTTP cookies stop working.
I've looked at a lot of threads regarding this issue and can't find a solution, no matter what I try, the cookie will not save to my browser. Here are the current things I have tried so far with no luck:
I've tried specifying the domain on the cookie. Both with a . and without
I've updated my host file to use a domain as a mask for localhost. Something like myapp.com:4444 which points to localhost:4444
I tried some fancy configuration I found where I was able to hide the port as well, so myapp.com pointed to localhost:4444.
I've tried Chrome, Safari, and Firefox
I've made sure there were no CORS issues
I've played around with the security settings of the cookie.
I also set up a ngrok server so there was a published domain to run in the browser
None of these attempts have made a difference so I am a bit lost at what to do at this point. The only other thing I could do is deploy my app to a proper server and just run my development off that but I really really don't want to do that, I should be able to develop from my local machine I would think.
My cookie knowledge is a bit bare so maybe there is something obvious I am missing?
This is what my setup looks like right now:
On the API I have a route(/refresh_token) that will create a new express cookie like so:
export const sendRefreshToken = (res: Response, token: string): void => {
res.cookie('jid', token, {
httpOnly: true,
path: '/refresh_token',
});
};
Then on the frontend it will essentially run this call on load:
fetch('http://localhost:3000/refresh_token', {
credentials: 'include',
method: 'POST'
}).then(async res => {
const { accessToken } = await res.json()
setState({ accessToken, workspaceId })
setLoading(false)
})
It seems super simple to do but everything just stops working when on a subdomain. I am completely lost at this point. If you any ideas, that would be great!

if httpOnly is true, it won't be parsable through client side js. for working with cookies on subdomains, set domain as the main domain (xyz.com)
an eg in BE:
res.cookie('refreshToken', refreshToken, {
domain: authCookieDomain,
path: '/',
sameSite: 'None',
secure: true,
httpOnly: false,
maxAge: cookieRefreshTokenMaxAgeMS
});
and on FE add withCredentials: true as axios options or credentials: include with fetch, and that should work

Related

Is it impossible to set a domain of localhost cookie remotely from a backend?

I've already checked out these two SO questions:
Can I use localhost as the domain when setting an HTTP cookie?
Setting a cookie from a remote domain for local development
But I don't want to edit my HOSTS file and setting a wildcard domain doesn't help me.
I've used node.js, but it should be programming language agnostic...
So my problem is the following:
I wanna work on my Angular frontend on https://localhost:4200 (and possibly http://localhost:4200) and reach my backend by getting access to it. Obviously I have to implement CORS rules for that, hence I've implemented the following CORS rules in the Node.js backend:
const allowedOrigins = environment.header;
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
where allowedOrigins is an array that contains the following:
environment.header = ['https://localhost:4200', 'http://localhost:4200', 'http://test.example.org', 'https://test.example.org'];
The problem at hand is that when I go to work on my Angular frontend locally it does not send the cookie to the backend for some reason (maybe this kind of connection is simply not allowed by some RFC???), hence my JWT checking mechanism throws 403 Forbidden after logging in instantly.
My JWT check function looks like this:
if (req.headers.origin === 'https://localhost:4200' || 'http://localhost:4200')
orig = 'localhost';
else
orig = req.headers.origin;
res.cookie(
'access_token', 'Bearer ' + token, {
//domain: 'localhost',
domain: orig,
path: '/',
expires: new Date(Date.now() + 900000), // cookie will be removed after 15 mins
httpOnly: true // in production also add secure: true
})
I need to do this to work on my Angular frontend locally and the backend has a connection to another server, which works only locally for now...
withCredentials is of course true (so the JWT cookie is being sent with), so that's not the problem in my codebase.
UPDATE
Ok so I've figured out that req.headers.origin is usually undefined..
UPDATE 2
Changed req.headers.origin to req.headers.host, but still it doesn't work
I needed to add the following properties to the res.cookie for it to work:
sameSite: 'none', secure: true
Then I enabled third-party cookies in Incognito mode and it worked for me.
Case closed.

Ouath Social Logins in React JS and Node(Express) with different sub domains

I have created an app in React and Node(Express) which seems to be working fine on local but I am facing issues on hosting them.
I have option to login using OAuth2(Google, LinkedIn, Twitter, GitHub). I have hosted the client on front.domain.io and server on api.domain.io. When I am clicking on login with google it is redirecting me to an invalid URL. Can someone help me to solve this. Below is my code and the expected URL and current redirection happening.
React Code which redirects to google login page :
const googleReqHandler = () => {
window.open(`${APIURL}/auth/google`, "_self");
};
Node Js Code to handle the redirection and callback
router.get(
"/google",
passport.authenticate("google", { scope: ["profile", "email"] })
);
router.get(
"/google/callback",
passport.authenticate("google", {
successRedirect: process.env.CLIENT_URL,
failureRedirect: "/login/failed",
})
);
Passport JS config
passport.use(
new GoogleStrategy(
{
clientID: GOOGLE_CLIENT_ID,
clientSecret: GOOGLE_CLIENT_SECRET,
callbackURL: `${process.env.APP_URL}/auth/google/callback`,
},
async function (accessToken, refreshToken, profile, done) {
const userProfile = profile;
await usersHelper.createOrUpdate(userProfile);
done(null, userProfile);
}
)
);
URLs that I am using in React And Node
APIURL : https://api.domain.io
CLIENT_URL : https://front.domain.io
APP_URL : https://api.domain.io
The URL that the user is being redirected from when the user clicks Login with Google button
https://api.domain.io/o/oauth2/v2/auth?response_type=code&redirect_uri=https%3A%2F%2Fapi.domain.io%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email&client_id=clientid
The URL it should have been redirected to
https://accounts.google.com/o/oauth2/v2/auth/identifier?response_type=code&redirect_uri=https%3A%2F%2Fapi.domain.io%2Fauth%2Fgoogle%2Fcallback&scope=profile%20email&client_id=clientid&flowName=GeneralOAuthFlow
I have configured the OAuth in all the developer consoles properly, it is working fine for all the logins on local.
I think that is issue is due to requests generation and the redirection from different subdomains where as on local it is same domain just different port(localhost:3000 and localhost:5000). So can someone help me to solve this issue. Thank you in advance.
So after not able to find much on the issue, I hosted the backend(node) using heroku and frontend(react) using netlify, after I found the video mentioned below after going through n number of videos and blogs, and everything worked fine. So I had a talk with the devops so got to know that most probably the issue was due to URL rewrites they had used in the IIS on windows server. So we hosted the application on ubuntu using nginx and then everything is working fine. So if any one faces such an issue do check for such URL rewrites if you are hosting it on windows.
Also make sure that the site is hosted on https, as http can give issues when on public domain(Someone had mentioned that in his video). Do add the following lines to your app.js in backend too or else they won't pass session cookies during login process when hosted on public domain with different domains.
app.use(
session({
secret: "whatever",
resave: true,
saveUninitialized: true,
cookie: {
sameSite: "none",
secure: true,
},
})
);
Here is the link of the video, which helped me to identify this issue and make required changes before hosting such setup as no other video which I referred to showed the hosting process.
Youtube video link

Set Cookie with Nextjs and Vercel

I have a NextJs app that is deployed with Vercel that is not setting the cookie. I went into the console > Network and can see the request has a 200 status and the set-cookie value is present with no warning. I check the console > Applications > Cookies and the cookie is not found. I found a few similar questions on StackOverflow and Github, but very limited answers that didn't seem to push me to a solution.
My domain structure is like this:
domain.com
api.domain.com
subdomain.domain.com
nextapp.domain.com -> deployed through vercel
The domain and subdomain apps are standard React apps deployed through AWS and the API is a nodejs (express) app. I'm having no issues with the cookies being set for the domain and subdomain apps so I'm led to believe this is an issue caused by Vercel. My API responses look like this:
res.status(200).cookie('token', token, { httpOnly: true }).json(...)
with these cors options set
app.use(cors({
origin: true,
credentials: true
})
I tried updating the cookie options in the response to the following, but neither had an effect.
{ httpOnly: true, sameSite: false, secure: true }
{ httpOnly: true, sameSite: 'none', secure: true }
Is there something within Vercel that I'm missing that I should be aware of?
EDIT
It looks like this could be an issue with Next.JS actually. I'm not using the API layer within Next.js. My request looks something like this:
try {
let response = await axios.post('https://api.domain.com/login', params, { withCredentials: true } )
} catch(error) {
...
}
The issue wasn't related to NextJS or Vercel. When I set my cookie on the api I needed to add the domain like this:
res.status(200).cookie('token', token, { httpOnly: true, domain: process.env.NODE_ENV === 'development' ? '.localhost' : '.domain.com' }).json(...)
and it set as expected. I'm not sure why the cookie was still being set regardless on my domain and subdomain requests, but nonetheless with the added domain it sets on my nextjs.domain

Passport-SAML: entryPoint / auth request header overridden

Alright you gurus, I need some help / understanding of what is happening here. I'm leveraging passport and passport-saml to do single sign on for my application. I have been able to get things working locally on my development machine, but when I deploy to our staging server, something is amiss and not leveraging the entryPoint URL that I have configured...
Example code:
return new Strategy(
{
callbackUrl: "https://my.domain.com/staging/api/login/callback",
entryPoint: "https://my.idp.com/affwebservices/public/saml2sso",
issuer: "my.domain.com",
cert: "THE SECRET SAUCE"
},
function(profile, done) {
// .....
}
)
// ROUTES ----------------------------------------------------------------------
app.get('/api/login', passport.authenticate("saml", { successRedirect: '/', failureRedirect: '/login' }) );
app.post('/api/login/callback', passport.authenticate("saml", { failureRedirect: '/', failureFlash: true }), (request, response) => {
// .....
});
When I run this locally, as stated, it works and I see the following SAML request being made:
https://my.idp.com/affwebservices/public/saml2sso?SAMLRequest=.....
However, once deployed, the entryPoint URL domain is overriden with the staging domain:
https://my.domain.com/affwebservices/public/saml2sso?SAMLRequest=.....
I'm noticing that the request generated is assuming authority to my.domain.com rather than utilizing my.idp.com:
I will say that the only difference between the development server and staging/prod server is that staging/prod is utilizing IIS as a reverse proxy to route incoming traffic based on URL string (i.e. my.domain.com/production, my.domain.com/staging). I've enabled CORS on the node server, which was how I got it working on the development server in the first place, as well as tried configuring IIS to allow for it too...
Stumped at this point. Any ideas?
Well after enough headbanging, I found a way to resolve the issue. As suspected, it was with IIS, and solution I implemented was a URL redirect:
I don't know if this is the most robust solution; so if someone else stumbled upon this, feel free to reach out. Either way, this works.

Not able to set cookie from the express app hosted on Heroku

I have hosted both frontend and backend in Heroku.
Frontend - xxxxxx.herokuapp.com (react app)
Backend - yyyyyy.herokuapp.com (express)
I'm trying to implement Google authentication. After getting the token from Google OAuth2, I'm trying to set the id_token and user details in the cookie through the express app.
Below is the piece of code that I have in the backend,
authRouter.get('/token', async (req, res) => {
try {
const result = await getToken(String(req.query.code))
const { id_token, userId, name, exp } = result;
const cookieConfig = { domain: '.herokuapp.com', expires: new Date(exp * 1000), secure: true }
res.status(201)
.cookie('auth_token', id_token, {
httpOnly: true,
...cookieConfig
})
.cookie('user_id', userId, cookieConfig)
.cookie('user_name', name, cookieConfig)
.send("Login succeeded")
} catch (err) {
res.status(401).send("Login failed");
}
});
It is working perfectly for me on my local but it is not working on heroku.
These are the domains I tried out already - .herokuapp.com herokuapp.com. Also, I tried out without specifying the domain field itself.
I can see the Set-Cookie details on the response headers but the /token endpoint is failing without returning any status code and I can't see the cookies set on the application tab.
Please see the below images,
I can't see any status code here but it says it is failed.
These are cookie information that I can see but it is not available if I check via application tab.
What am I missing here? Could someone help me?
May you should try secure as:
secure: req.secure || req.headers['x-forwarded-proto'] === 'https'
You are right, this should technically work.
Except that if it did work, this could lead to a massive security breach since anyone able to create a Heroku subdomain could generate a session cookie for all other subdomains.
It's not only a security issue for Heroku but also for any other service that lets you have a subdomain.
This is why a list of domains has been created and been maintained since then to list public domains where cookies should not be shared amongst the subdomains. This list is usually used by browsers.
As you can imagine, the domain heroku.com is part of this list.
If you want to know more, this list is known as the Mozilla Foundation’s Public Suffix List.

Resources