Cooke is being stored on both localhost:3001 and localhost:3000? - node.js

I am using PassportJS, and using passport-discord for oauth login.
I don't have any real issues with this flow so far although a major question arises.
authRouter.get('/discord', passport.authenticate('discord', { session: false }))
authRouter.get('/discord/callback', passport.authenticate('discord', { session: false }), (req, res) => {
const token = jwt.sign(req.user , 'SECRET')
res.cookie('token', token, {
httpOnly: true,
maxAge: 60000
})
res.redirect(`http://localhost:3000`)
})
For some odd reason, once I get redirected to my main website at localhost:3000, the cookie is being stored there normally as it should. However if I go to localhost:3001/ I check my cookies and see that a cookie is also being stored there as well.
Is this to be expected? It seems like a major flaw both in terms of logic and security?
I am following this exactly: https://www.passportjs.org/packages/passport-discord/
I am using cookie-parser as well
var DiscordStrategy = require('passport-discord').Strategy;
var scopes = ['identify', 'email', 'guilds', 'guilds.join'];
passport.use(new DiscordStrategy({
clientID: 'id',
clientSecret: 'secret',
callbackURL: 'callbackURL',
scope: scopes
},
function(accessToken, refreshToken, profile, cb) {
return cb(null, profile)
}));

For some odd reason
It's not odd, it's how cookies work and always worked. The cookie is bound to the domain, not the port. It's not a security flaw. In production, you use only two ports anyway, either 80 or 443. You won't have an application that listens on port 3000 exposed directly to your users.
If you need to limit cookies to one app during development you can either configure domains that you point to localhost (see the link #Aziz added in the comment), or set the path attribute on cookies. This way the cookie is sent by the browser only when calling the given path.

Related

Unable to verify authorization state on Heroku

I'm running a nodejs/reactjs app on heroku. I implemented google login using Passport. I'm getting an error "Unable to verify authorization state" when people try to login.
I see here NodeJS Express Session isn't being restored between routes in kubernetes that I need to set the X-Forwarded-SSL header. How do I do that according to what the question says?
The solution outlined on that page also mentions Apache, but Heroku doesn't make use of Apache to forward requests to apps' web dynos.
Is anyone running into the same issue on Heroku?
So the weird thing is when I try to login, it works the second time but the first time, I get the error "Unable to verify authorization state".
here's my index.js
const session = require("express-session");
app.use (
session ({
secret: "ddd",
resave: false,
saveUninitialized: true,
cookie: {
expires: 60 * 60 * 24,
secure: (app.get('env') === 'production')
}
})
);
if (app.get('env') === 'production') {
app.set('trust proxy', 1); // trust first proxy
}
So as I mentioned your issue might not be related to the request headers because in my issue, session never persisted, whereas yours does in your second attempt. It might be an issue with your verify function or your deserializeUser function.
Here's an example. I don't use Google auth personally, but something else. My code looks similar, but I got some of the Google auth code from https://github.com/jaredhanson/passport-google/blob/master/examples/signon/app.js. Fill in your stuff where appropriate and debug/log what's coming in to see how your functions are being called.
passport.use(new GoogleStrategy({
returnURL: 'http://localhost:3000/auth/google/return', // replace with yours
realm: 'http://localhost:3000/' // replace with yours
}, function (identifier, profile, done) {
// console.log identifier and profile, or even better, use your debugger on this line and see what's happening
// Do whatever you need to do with identifier and then..
return done(null, profile);
}));
passport.serializeUser(async (user, done) => {
// console.log user or use your debugger on this line and see what's happening
const userInDb = await db.getUser(user) // Find your user in your db using based on whatever is in the user object
const serializedSessionUser = {id: userInDb.id, username: userInDb.username} // this is the object that'll appear in req.user. Customize it like you want
return done(null, serializedSessionUser); // Commit it to the session
});
passport.deserializeUser((user, done) => {
done(null, user);
});
Edit: Apparently there's 2 ways to use Google for passport. The first is OAuth / OAuth 2.0. The second is OpenID. I used OpenID in this example. Adjust accordingly!
Edit 2: Here's my own equivalent to your index.js:
app.use(session({
cookie: {
sameSite: 'lax',
secure: !['development', 'test'].includes(process.env.NODE_ENV),
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
},
proxy: true,
resave: false,
saveUninitialized: true,
secret: process.env.COOKIE_SECRET,
store: 'your store',
rolling: true,
}));

Can't get session to save/persist on deployed express server

My attempts at logging in are not getting saved to express session in production. I am saving the session in Mongo Store and the sessions are coming up in MongoAtlas as modified (they way they should appear), but for some reason the server is not recognizing that there is an existing session and is making a new one. When I enable express-session debug, it logs express-session no SID sent, generating session on each request to the server. This makes me think that the session id isn't getting sent with the request and that the problem has something to do with my client and server being on different domains (my client address is https://example.com and my server is on https://app.example.com. I originally had my client on https://www.example.com but changed it thinking that the cookie was getting mistaken for a 3rd party cookie (maybe it still is).
My client is hosted on Firebase Hosting and my Express server is hosted on Google Cloud Run
my express-session settings
app.set('trust proxy', true)
app.use(session({
secret: 'myappisasecret',
resave: false,
saveUninitialized: false,
secure: true,
store: new MongoStore({mongooseConnection: mongoose.connection}),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 7, // 1 week
sameSite: 'lax',
secure: true,
domain: 'mysite.com'
},
proxy: true // I think this makes the trust proxy be useless
}))
Below is my coors server stuff. This code is located above the code above, but I don't think it is causing any issues, but think that it might be important to include.
let whitelist = ['https://app.example.com', 'https://www.example.com', 'https://example.web.app', 'https://example.com']
let corsOptions = {
origin: (origin, callback) => {
if (whitelist.indexOf(origin) !== -1 || origin === undefined) {
callback(null, true)
} else {
console.log('Request Origin blocked: ', origin)
callback(new Error('Request blocked by CORS'))
}
},
credentials: true
}
app.use(cookieParser('myappisasecret'))
app.use(cors(corsOptions))
Since the server wasn't receiving a session id, I thought that maybe my client wasn't sending one so I added credentials: 'include' to my client request code
const reqHeaders = {
headers: {
"Content-Type": "application/json"
},
credentials: 'include' as any,
method: "GET"
}
fetch('https://app.example.com/u/loggedIn', reqHeaders)
.then(res => etc...
When this request gets submitted expression-session debug logs:
express-session saving z3ndMizKoxivXR0N9LBZYkPhDG65uvF2 and then
express-session split response
This makes me think that as it tries to save my user data to the session, it gets overwritten at the same time with an initial session data. I have set resave: false. But even then I still get express-session no SID sent with every request sent to the server.
Apparently when hosting with Firebase and Cloud Run cookie headers get stripped due to Google's CDN cache behavior.
Here's the documentation that describes that:
https://firebase.google.com/docs/hosting/manage-cache#using_cookies
I have no clue how to implement sessions now. F

Handling Query string/parameters in nodejs after Okta sign-in

I have a angular and Node(with express framework) application. I am using okta for authentication. This angular application opens up with different query parameters for eg. www.mysite.com/home?tab=1. I have setup routes in angular which opens different pages based on the value of tab. But in Okta redirect url has to be static so I have setup the redirect url as www.mysite.com/home. The query parameter info is lost after okta redirects back to the application. How can I get the tab=1 in node js. I am using passport and passport-openidconnect.
app.use(
session({
secret: crypto.randomBytes(64).toString('hex').substring(0, 20),
resave: true,
saveUninitialized: true,
})
);
app.use(passport.initialize());
app.use(passport.session());
passport.use(
'oidc',
new OidcStrategy(
{
issuer: '',
authorizationURL: 'xxxx',
tokenURL: 'xxxx',
userInfoURL: 'xxxx',
clientID: 'xxxx',
clientSecret: 'xxxx',
callbackURL: 'xxxx',
scope: 'profile groups',
nonce: crypto
.randomBytes(64)
.toString('hex')
.substring(0, 20),
},
(issuer, sub, profile, accessToken, refreshToken, params, done) => {
return done(null, profile);
}
)
);
app.use('/mdi', passport.authenticate('oidc'))
you'll need to do another request to the backend in order to get more informatión about the user. This could be useful to get data that determine which is the tab you need to show depending on the user.
Eg: you could send a request to your backend "www.yourbackend/me" and in this endpoint return the tab value based on the session.
Another possibility is to get a jwt with user-data in its payload, however, with this approach you don´t have sessions in the backend anymore.

Unable to redirect to client URL from server

I am currently building a React application which requires google login.
As I'm trying to build a server which can also later serve PWA, I'm planning on doing the authentication using JWT.
The problem that's occurring is that I'm not being able to redirect my app to the client URL from the server.
For example, suppose my client is running on localhost:8100 and the server is running on localhost:3100.
I have set up proxy on my react app to forward all request to /auth/* to the server.
After the auth is complete, when I try to redirect from my server to client using res.redirect("/handleAuth/$token"), I am being redirected to localhost:3000/handleAuth/$token instead of localhost:8100/handleAuth/$token
Here is the server code
const passport = require("passport");
passport.use(
new GoogleStrategy(
{
...keys.google,
callbackURL: "/auth/google/callback",
proxy: true
},
async (accessToken, refreshToken, { _json }, done) => {
done(null, _json);
}
)
);
app.get(
"/auth/google",
passport.authenticate("google", {
session: false,
scope: ["openid", "profile", "email"]
})
);
app.get(
"/auth/google/callback",
passport.authenticate("google", { session: false }),
(req, res) => {
const token= generateToken(req.user);
// Redirecting to serverUrl/handleAuth/${token} instead of clientURL/handleAuth/${token}
res.redirect(`/handleAuth/${token}`)
}
);
Since I'm using JWT, I haven't initialized session on the server.
If there is any approach where I'll be able to open another child window where login occurs and I can send the response as json which could read by the parent window, or any other way that can be also used by native android/ios apps which is more decent than this, then please share.
Okay, so I found out what the problem was.
While setting proxy in react, set changeOrigin to false.
It used to work earlier with changeOrigin set to true, but now that it's changed, here's the fix.

passport-saml - express - redirected url not submitting form gives SAML assertion not yet valid

Below is the error that I am getting on my console today as opposed to yesterday when the same code was working fine.
Error: SAML assertion not yet valid
at SAML.checkTimestampsValidityError
I have verified that I receive a success from the IDP and hence the
application gets redirected to the '/home' endpoint in the URL which has been
mentioned in the config file.
Additionally, when I submit the form, after an auto redirection [which shows me Internal Server Error]
I press refresh button of the browser and a form submission happens and the expected result is achieved.
My problem is, why doesn't this happens automatically or how and where can I do this submission programatically.
passport.js
const SamlStrategy = require('passport-saml').Strategy;
module.exports = function (passport, config) {
passport.serializeUser(function (user, done) {
done(null, user);
});
passport.deserializeUser(function (user, done) {
done(null, user);
});
passport.use(new SamlStrategy(
{
entryPoint: config.passport.saml.entryPoint,
issuer: config.passport.saml.issuer,
cert: config.passport.saml.cert,
path: config.passport.saml.path,
identifierFormat: config.passport.saml.identifierFormat
},
function (profile, done) {
debugger;
return done(null,
{
sessionIndex: profile.sessionIndex,
nameID: profile.nameID,
lastName: profile.lastName,
firstName: profile.firstName,
gid: profile.gid,
county: profile.county,
mail: profile.mail,
companyUnit: profile.companyUnit,
preferredLanguage: profile.preferredLanguage,
orgCode: profile.orgCode,
email: profile.email
});
})
);
};
config.js
module.exports = {
passport: {
strategy: 'saml',
saml: {
callbackUrl: '/home',
path: '/home',
entryPoint: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP',
issuer: '...',
cert: '...',
identifierFormat: null
}
}
};
app.js
import express from 'express';
import helmet from 'helmet';
import cookieParser from 'cookie-parser';
import bodyparser from 'body-parser';
import path from 'path';
import logger from 'morgan';
import cors from 'cors';
import passport from 'passport';
import session from 'cookie-session';
const config = require('./config.js');
require('./passport')(passport, config);
var app = express();
app.use(logger('dev'));
app.use(cookieParser());
app.use(bodyparser.json());
app.use(bodyparser.urlencoded({ extended: false }));
app.use('/public', express.static(path.join(__dirname, '../public')));
app.use('/data', express.static(path.join(__dirname, '../uploads/')));
app.use(session(
{
resave: true,
saveUninitialized: true,
secret: 'secret value'
}));
app.use(passport.initialize());
app.use(passport.session());
app.use(helmet());
app.use(cors());
require('../router/routeConfig')(app, config, passport);
module.exports = app;
routeConfig.js
module.exports = function (app, config, passport) {
app.get('/', passport.authenticate(config.passport.strategy, {
successRedirect: '/home',
failureRedirect: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP'
}));
app.get('/app', passport.authenticate(config.passport.strategy, {
successRedirect: '/home',
failureRedirect: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP'
}));
app.post(config.passport.saml.path,
passport.authenticate(config.passport.strategy,
{
failureRedirect: 'https://.../GetAccess/Saml/IDP/SSO/Unsolicited?GA_SAML_SP=APP',
failureFlash: true
}),
function (req, res) {
debugger;
res.sendFile(path.join(__dirname, "../public/index.html"));
});
};
Finally I have figured this out after some research,
As we understand that there are two parties involved in the SAML Authentication Process i.e. IDP and SP, therefore there are certain conditions between these which are supposed to be met, as part of the contract. One such condition is the TIME.
<saml:Conditions NotBefore="2019-08-01T11:02:49Z" NotOnOrAfter="2019-08-01T11:03:59Z">
This is a tag that I have clipped from the saml response received from the IDP, here the time of my(SP's) server should be between NotBefore and NotOnOrAfter during the process of authentication.
Therefore, I need to calibrate the clock of my server by a few seconds so that I fit in the time-slice of NotBefore and NotOnOrAfter of the server.
Of course that is not the way this should be done, but some +n or -n minutes should be allowed from the IDP side (importantly both SP and IDP to follow UTC times).
More on this topic can be found here,
SAML Assertion NotBefore,NotOnOrAfter problem due to unsynced clocks : Explained
ADFS Not Before Time Skew
Bonus
Base 64 to XML Decoder
XML Prettifier
Edit :
As mentioned in the comment below, The skew can be configured on either side (IdP > or SP) or both sides. From passport-saml docs:
acceptedClockSkewMs: Time in milliseconds of skew that is acceptable
between client and server when checking OnBefore and NotOnOrAfter
assertion condition validity timestamps. Setting to -1 will disable
checking these conditions entirely. Default is 0.
Mentioned Here
As described above, adding 'acceptedClockSkewMs: -1' to your passport-saml strategy configuration resolves the error.
example:
const strategy = new SamlStrategy(
{
path: "/api/auth/callback",
entryPoint: process.env.SAML_ENTRY_POINT, // identity provider entrypoint
issuer: issuer,
cert: process.env.SAML_CERT,
acceptedClockSkewMs: -1
}, function(profile, done){...}
EDIT: Looking back at this answer I should have gone into more detail and highlighted the pros/cons and different approaches.
Do not use the suggested -1 or default 0 from the passport-saml documentation. Increase the ms time to 10 or tinker with the value until it suits your environment and latency between server, SP and IDP.
I do not have enough reputation to add comment to #adR 's answer (which instructed to set acceptedClockSkewMs to -1 in order to fix the problem) so I'm posting a separate answer.
Setting acceptedClockSkewMs to -1 is not proper a fix at all. It opens a replay attack vector.
The reason is that passport-saml skips NotOnOrAfter validation if acceptedClockSkewMs is set to -1.
A proper fix is (see #Prateek 's answer for more information): Keep the clocks in sync by using e.g. NTP and use acceptedClockSkewMs to finetune small time differences (e.g. 30 seconds).
By disabling the NotOnOrAfter check, an attacker may replay a stored SAML login response forever without ever having to authenticate at IdP anymore (i.e. it would be possible to gain access to SP even after account at IdP side is terminated).
Side note: If a disabled NotOnOrAfter validation is combined with a disabled audience validation (it is disabled by default in passport-saml meaning that it is disabled in #adR 's example also) any stored saml authentication response from any SP (which share same IdP) can be used to gain access by replaying login response to site with aforementioned disabled checks.

Resources