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

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.

Related

Cooke is being stored on both localhost:3001 and localhost:3000?

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.

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,
}));

keycloak-connect nodejs / meteor - Getting access denied at first login only and prod only

I have a meteor/nodeJs app that needs to connect to my client to authentify. I set up a connection access point as such (I just anonymized the various values):
import Keycloak from "keycloak-connect";
import { WebApp } from "meteor/webapp";
import express from "express";
import session from "express-session";
const app = express();
const memoryStore = new session.MemoryStore();
app.use(
session({
secret: "secret",
resave: false,
saveUninitialized: true,
store: memoryStore,
})
);
const kcConfig = {
clientId: "clientId",
serverUrl: "realmUrl",
realm: "clientName",
realmPublicKey: "publicKey",
};
const keycloak = new Keycloak({ store: memoryStore }, kcConfig);
app.use(keycloak.middleware());
app.get("/connect", keycloak.protect(), (req, res) => {
// doing my stuff here
res.writeHead(301, {
Location: "/connected",
});
res.end();
});
WebApp.connectHandlers.use(app);
The problem is:
When I run my server locally and go to the /connect link, I am redirected to the connection platform. I connect and I am sent back to my localhost:3000/connected => Everything works as intended
when I do exactly the same flow on the production environment I am getting an access denied (blank page with only access denied written) after trying to login for the first time. If I then manually go back to the /connect link I am getting directly connected (I guess I got the token properly and could connect again)
I don't know why the behaviour is different on both environment and why I am getting an access denied page when in prod.
As mentioned in comments I had an issue with my ROOT_URL, a trailing slash was left. Went better after removing it.

How to access HTTP Cookie in Node.JS - JWT

This might seem like a redundant question, but please hear me out first:
I'm working with a React Frontend and a Node Backend. I'm using JWT to deal with user authentication. Right now, I'm having trouble actually working with the JWT and performing the authentication. Here's where I'm stuck:
~ I try setting the token as an http cookie in my backend. If i work with postman, I see the token being set. However, when I use req.cookies.token to try and receive the token cookie to perform validation in the backend, I get an undefined value. Am I supposed to be sending the cookie from the frontend to the backend somehow? I feel like this is the part that I am missing.
Please advise!
SO I can give you an alternative solution to handling session making use of express-session and connect-mongodb-session this has tend to been the popular and somewhat secure solution for server session handling
Firstly you will need the following packages
npm i express-session connect-mongodb-session or yarn add express-session connect-mongodb-session
Now that we have packages that we need to setup our mongoStore and express-session middleware:
//Code in server.js/index.js (Depending on your server entry point)
import expressSession from "express-session";
import MongoDBStore from "connect-mongodb-session";
import cors from "cors";
const mongoStore = MongoDBStore(expressSession);
const store = new mongoStore({
collection: "userSessions",
uri: process.env.mongoURI,
expires: 1000,
});
app.use(
expressSession({
name: "SESS_NAME",
secret: "SESS_SECRET",
store: store,
saveUninitialized: false,
resave: false,
cookie: {
sameSite: false,
secure: process.env.NODE_ENV === "production",
maxAge: 1000,
httpOnly: true,
},
})
);
Now the session middleware is ready but now you have to setup cors to accept your ReactApp so to pass down the cookie and have it set in there by server
//Still you index.js/server.js (Server entry point)
app.use(
cors({
origin: "http://localhost:3000",
methods: ["POST", "PUT", "GET", "OPTIONS", "HEAD"],
credentials: true,
})
);
Now our middlewares are all setup now lets look at your login route
router.post('/api/login', (req, res)=>{
//Do all your logic and now below is how you would send down the cooki
//Note that "user" is the retrieved user when you were validating in logic
// So now you want to add user info to cookie so to validate in future
const sessionUser = {
id: user._id,
username: user.username,
email: user.email,
};
//Saving the info req session and this will automatically save in your mongoDB as configured up in sever.js(Server entry point)
request.session.user = sessionUser;
//Now we send down the session cookie to client
response.send(request.session.sessionID);
})
Now our server is ready but now we have to fix how we make request in client so that this flow can work 100%:
Code below: React App where you handling logging in
//So you will have all your form logic and validation and below
//You will have a function that will send request to server
const login = () => {
const data = new FormData();
data.append("username", username);
data.append("password", password);
axios.post("http://localhost:5000/api/user-login", data, {
withCredentials: true, // Now this is was the missing piece in the client side
});
};
Now with all this you have now server sessions cookies as httpOnly

Passport.js / Express.js Creating New Session on Every Network Request

I have a working login function that properly authenticates and saves. However, express never remembers the old session, it always creates a new one for every network request.
Evidently, Passport is exceedingly sensitive to the order that express middleware is initialized. (Example: https://www.airpair.com/express/posts/expressjs-and-passportjs-sessions-deep-dive). I checked my config against a number of examples and rearranged it to so if it would help, but it hasn't moved my bug. Either that isn't the issue or I just haven't found the config holy grail yet. Here's my current config:
Express Config
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'html');
app.use(express.static(path.join(config.root, '/views')));
app.set('views', config.root + '/views');
var sessionOpts = {
saveUninitialized: true,
resave: false,
store: new RedisStore({
host: 'localhost',
port: 6379
}),
secret: 'keyboard',
cookie: {
httpOnly: true,
maxAge: 1000
}
}
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({
extended: false
}));
app.use(cookieParser('keyboard'));
app.use(session(sessionOpts));
app.use(passport.initialize());
app.use(passport.session());
app.use(cors());
require('./routes/router.js')(app, passport);
Passport Config
passport.use('local-login', new LocalStrategy({
usernameField: 'email',
passwordField: 'password',
passReqToCallback: true
},
function(req, username, password, done) {
client.hgetall(username, function(err, reply) {
if (err) {
return done(err);
}
if (!reply) {
return done(null, false, {
message: 'Incorrect username.'
})
}
if (reply.password !== password) {
return done(null, false, {
message: 'Incorrect password.'
})
}
return done(null, reply)
})
}));
Does Passport need handholding for Redis? Redis sessions are stored in a 'sess' folder with a key like so: sess:RhodmaK2V2wDNLglV5j1B6rC. All of the tutorials I've found have been about Mongo so I'm not sure if I need to somehow include the session key when trying to look it up. Within the session entry, it's properly stored in standard cookie form for passport though: req.session.passport.user
Is there any way to see what is happening inside of passport initialize? On subsequent requests it is supposed to do this: "The general passport middleware we setup (passport.initialize) is invoked on the request, it finds the passport.user attached to the session. If is doesn't (user is not yet authenticated) it creates it like req.passport.user = {}." See 'SUBSEQUENT AUTHENTICATED REQUESTS FLOW' - http://toon.io/understanding-passportjs-authentication-flow/ I strongly suspect my problem lies at that step, so it would be nice to be able to see inside of it.
Some interesting tidbits:
Passport has never once called deserializeUser. I assume it's never
reached that point.
I have checked the other StackOverflow questions about this problem,
but none of those solutions worked for me.
I've checked to see if the new sessions are generated by any static resources but they are not. It's only for intentional server requests. On the pages without them, no new sessions are made.
All sessions have either a populated or empty req.session.passport property.
Your session ID might be different for every request because you have your cookie set to expire after 1 second. cookie.maxAge here is in ms:
cookie: {
httpOnly: true,
maxAge: 1000
}
Edit: I also have to nag you about storing passwords in plaintext. Don't do this ;)

Resources