passport saml how to pass profile data to route - express

when I created passport-saml strategy, during login, there is a profile object pass to the middleware function, with nameID info there. I need that info to call logout later on.
// passportHandler.js
const passport = require("passport");
const passportSaml = require("passport-saml");
passport.serializeUser((user, done) => {
done(null, user);
});
passport.deserializeUser((user, done) => {
done(null, user);
});
// SAML strategy for passport -- Single IPD
const samlStrategy = new passportSaml.Strategy(
{
entryPoint: process.env.SSO_ENTRYPOINT,
logoutUrl: process.env.SSO_LOGOUT,
issuer: process.env.SSO_ISSUER,
callbackUrl: process.env.SSO_CALLBACK_URL || undefined,
path: process.env.path,
cert: process.env.SSO_CERT.replace(/\\n/gm, "\n"), // change "\n" into real line break
},
(profile, done) => {
console.log('profile', profile); // nameID and nameIDFormat are in profile object
done(null, profile)
}
);
passport.use(samlStrategy);
module.exports = passport;
index.js
// index.js of Express server
import passport from "./src/passportHandler";
import { getLogout } from "./src/routes.js";
const app = express();
app.use(passport.initialize());
app.use(passport.session());
app.get('/sso/logout', getLogout); // this route, I need the above 2 data
getLogout function import from another file, I hardcode nameID and nameIDFormat, how do I get them from the beginning profile object, save them somewhere, and pass them to this route?
// routes.js
export const getLogout = (req, res) => {
!req.user && (req.user = {})
req.user.nameID = 'Eric1234#outlook.onmicrosoft.com'; // hardcode, how to pass this info?
req.user.nameIDFormat = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'; // hardcode too
const samlStrategy = req._passport?.instance?._strategies?.saml; // is this correct?
samlStrategy.logout(req, (err, request) => {
if (!err) {
res.redirect(request);
}
})
};
my second question is, I get the samlStrategy object from req._passport?.instance?._strategies?.saml, is it a proper way to get it? or, again the similar question, how can I pass saml strategy obj from the beginning create logic to this route?
thanks for any help!

answering my own silly question...
in samlStrategy, at last calling done(null, profile)
const samlStrategy = new passportSaml.Strategy(
{
entryPoint: process.env.SSO_ENTRYPOINT,
logoutUrl: process.env.SSO_LOGOUT,
issuer: process.env.SSO_ISSUER,
callbackUrl: process.env.SSO_CALLBACK_URL || undefined,
path: process.env.path,
cert: process.env.SSO_CERT.replace(/\\n/gm, "\n"), // change "\n" into real line break
},
(profile, done) => {
console.log('profile', profile); // nameID and nameIDFormat are in profile object
done(null, profile)
}
);
then the profile object will become req.user object in the Service Provider's Login Post Callback function
Then I can save the user object somewhere, and use it again when logout being called.

Related

Using Passport.js, cookie not persisting from Heroku Rest API backend to Netlify React frontend

So I feel like I've tried everything.
Quick workflow breakdown. I have a React App deployed on Netlify that uses a Rest API backend hosted on Heroku with a Jaws MySQL instance and a Redis session store. There is an authorization workflow that uses a google0auth2.0 strategy and passport.js as well as the Redis to store each separate session which I only started using on the production build as my understanding was that express-sessions alone would cause memory leaks. That was my first step through this rabbit hole.
After some research, I added app.set('trust proxy', 1), added app.use(helmet.hsts()) to address headers issues and I attempted to secure the cookie with the code below:
app.use(
expressSession({
...
proxy: isProdEnv,
cookie: {
secure: isProdEnv
}
When checking the network tab in chrome dev tools, I see the cookie is attached to the callback however, it is not attached to the dashboard react page that the API redirects on the successful authorization.
My API index.js is below:
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
const expressSession = require('express-session');
const {
createClient
} = require("redis");
let RedisStore = require('connect-redis')(expressSession);
require('dotenv').config();
const PORT = process.env.PORT || 5050;
const isProdEnv = process.env.NODE_ENV === 'production' ? true : false;
// Knex instance for DB managment
const knex = require('knex')(require('./knexfile.js')[process.env.NODE_ENV || 'development']);
const app = express();
app.use(express.json());
app.use(helmet());
app.use(helmet.hsts());
app.use(
cors({
origin: true,
credentials: true,
}),
);
let redisClient = createClient({
url: process.env.REDIS_URL,
lazyConnect: true,
showFriendlyErrorStack: true,
legacyMode: true,
retry_strategy: (options) => {
const {
error,
total_retry_time,
attempt
} = options;
if (error ? .code === 'ECONNREFUSED' || error ? .code === 'NR_CLOSED') {
return 5000;
}
if (total_retry_time > 1000 * 15) {
return undefined;
}
if (attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 1000, 5000); //in ms
},
});
if (!redisClient.isOpen) {
redisClient.connect().catch(console.error);
console.info('connected to redis at', process.env.REDIS_URL);
}
redisClient.on('error', (err) => {
console.log('ⓘ on error:', err);
});
app.set('trust proxy', 1);
// Include express-session middleware (with additional config options required
// for Passport session)
app.use(
expressSession({
store: new RedisStore({
client: redisClient
}),
secret: process.env.SESSION_SECRET,
proxy: isProdEnv,
resave: false,
saveUninitialized: true,
name: 'lostnfound',
cookie: {
secure: isProdEnv
}
}),
);
// =========== Passport Config ============
// Initialize Passport middleware
app.use(passport.initialize());
app.use(passport.session());
passport.use(
new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ['profile', 'email'],
},
(_accessToken, _refreshToken, profile, done) => {
const id = String(profile.id);
const profileId = Number(id.slice(-18));
// First let's check if we already have this user in our DB
knex('users')
.select('id')
.where({
google_id: profileId
})
.then((user) => {
if (user.length) {
// If user is found, pass the user object to serialize function
done(null, user[0]);
} else {
// If user isn't found, we create a record
knex('users')
.insert({
google_id: profileId,
avatar_url: profile._json.picture,
first_name: profile.name.givenName,
last_name: profile.name.familyName,
email: profile._json.email,
})
.then((userId) => {
// Pass the user object to serialize function
done(null, {
id: userId[0]
});
})
.catch((err) => {
console.log('Error creating a user', err);
});
}
})
.catch((err) => {
console.log('Error fetching a user', err);
});
},
),
);
// `serializeUser` determines which data of the auth user object should be stored in the session
// The data comes from `done` function of the strategy
// The result of the method is attached to the session as `req.session.passport.user = 12345`
passport.serializeUser((user, done) => {
console.log('serializeUser (user object):', user);
// Store only the user id in session
done(null, user.id);
});
// `deserializeUser` receives a value sent from `serializeUser` `done` function
// We can then retrieve full user information from our database using the userId
passport.deserializeUser((userId, done) => {
console.log('deserializeUser (user id):', userId);
// Query user information from the database for currently authenticated user
knex('users')
.where({
id: userId
})
.then((user) => {
// Remember that knex will return an array of records, so we need to get a single record from it
console.log('req.user:', user[0]);
// The full user object will be attached to request object as `req.user`
done(null, user[0]);
})
.catch((err) => {
console.log('Error finding user', err);
});
});
// Additional information on serializeUser and deserializeUser:
// https://stackoverflow.com/questions/27637609/understanding-passport-serialize-deserialize
// Import all route types for server functionality
const authRoutes = require('./routes/auth');
const postRoutes = require('./routes/post');
app.use('/auth', authRoutes);
app.use('/post', postRoutes);
app.listen(PORT, () => {
console.log(`🚀 Server listening on port ${PORT}.`);
});
And this is my google endpoint and google callback:
const express = require('express');
const router = express.Router();
const passport = require('passport');
require('dotenv').config();
const AuthController = require('../controller/auth');
// Create a login endpoint which kickstarts the auth process and takes user to a consent page
router.get('/google', passport.authenticate('google'));
// This is the endpoint that Google will redirect to after user responds on consent page
router.get(
'/google/callback',
passport.authenticate('google', {
failureRedirect: `${process.env.CLIENT_URL}/auth-fail`,
}),
(_req, res) => {
// Successful authentication, redirect to client-side application
res.redirect(`${process.env.CLIENT_URL}/dashboard`);
},
);
I will add, I'm on the free Heroku account and haven't set up any SSL. Is that what is holding me back? is there any third-party free SSL that I can work into the workflow?
It's hosted here if you want to get a first-hand look. Click on the login to google to go through the workflow and checkout the network tab after you're authorized (if you dare, lol)

Sequelize model property undefined Express.js controller after auth with passport-jwt

I am using passport-jwt to verify access to a given route in express.js, and then return a Sequelize model to the final controller. The code looks like:
The auth strategy:
const passportStrategy = passport => {
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: config.auth.ACCESS_TOKEN_SECRET
};
passport.use(
new Strategy(options, async (payload, done) => {
try {
const user = await User.findOne({ where: { email: payload.email }});
if (user) {
return done(null, {
user
});
}
return done(null, false);
}
catch (error) {
return done(error, false)
}
})
);
};
The route with the auth middleware
router.get('/:user_id/psy', passport.authenticate('jwt', { session: false }), patientsController.getPatientPsy);
The controller function
const getPatientPsy = async (req, res) => {
const authenticatedUser = req.user;
if (authenticatedUser.userType !== "patient") {
res.status(500).send("Big time error");
}
}
If I console.log(authenticatedUser) in the getPatientPsy() controller it successfully prints the Sequelize model with it's dataValues and so on, but when I try to access any property, be it userType or any other it consistently returns undefined.
In the passport-jwt authentication once a User has been found that matches the extracted JWT token, afaik it is returned synchronously and made it available in the req.user object, and I can print it with console.log, but why can't I access the model's properties?
I've tried to make the getPatientPsy() controller a sync function but it doesn't work either.
Thank you.
All right this is embarrassing, by default Passport.js returns the done(null, user) in the req.user property, and since I am returning { user }, I had to access through req.user.user.

How to access req.session in nextServerInit before rendering

I am setting up a website base on nuxt+express with cmd 'vue init nuxt-community/express-template'.The functionality i am working on is "auth".I am trying to get the session while i refresh the page so that to keep the store correctly(Just for user info).
After config the session(the middleware i use is 'express-session'), I set the session after 'login successfully' and console it correctly.However when i console the req.session in method nuxtServerInit, it gives me a 'undefined'.
// express-api
router.post('/login', async (req, res, next) => {
const { username, password } = req.body;
try {
const userQuery = await User.findOne({
username,
password: md5(password)
}).exec();
if (userQuery) {
req.session.user = userQuery
console.log(req.session)
return ResUtils.success(res, userQuery);
// store actions
nuxtServerInit({ commit }, { req, res }) {
console.log(req.session)
commit('initUserInfo', req.session.user)
I expect to get req.session.user with which i can initialize the store after refresh the page. unfortunately, req.session is undefined.
you need to add this in your server/index.js or server/app.js basically wherever you create your express instance
// Create express router
const router = express.Router()
// Transform req & res to have the same API as express
// So we can use res.status() & res.json()
router.use((req, res, next) => {
Object.setPrototypeOf(req, app.request)
Object.setPrototypeOf(res, app.response)
req.res = res
res.req = req
next()
})

benefit of deserializeUser method in passport.js

I have just started with passport.js. From this article, I got what is the flow of all the passport methods and implemented the same in my application and it is working. Here is my server.js and I am using passport-local strategy. Angular app and rest APIs on the same server
import { registerControllersFromFolder } from 'giuseppe';
import { MessageManager } from './messaging/MessageManager';
import express = require('express');
import bodyParser = require('body-parser');
import session = require("express-session");
import http = require('http');
// class to hold user info
class User {
userId: number;
userName: string;
constructor(userId: number, userName: string) {
this.userId = userId;
this.userName = userName;
}
}
// server class to create http server
export class Server {
// list of apis for which authentication is not required
private static publicApiList: string[] = ["/services/login", "/login", "/favicon.ico"];
// request interceptor that will check user authentication
private static isAuthenticated = (req, res, next) => {
console.log("Authenticating :", req.originalUrl);
if (req.isAuthenticated() || Server.publicApiList.indexOf(req.originalUrl) > -1) {
// express routing
if (req.originalUrl.startsWith("/services")) {
console.log("Express Routing");
return next();
} else { // angular routing -> return index.html
console.log("Angular Routing");
return res.sendFile(__dirname + "/public/index.html");
}
} else {
console.log("User not authenticated.")
res.redirect('/');
}
};
static startServer() {
let userList: User[] = [new User(1, "Sunil"), new User(2, "Sukhi")];
let app = express();
// passport library
let passport = require('passport');
let LocalStrategy = require('passport-local').Strategy;
// middlewares
app.use(express.static(__dirname + "/public"));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(session({ resave: false, saveUninitialized: true, secret: "secretKey123!!" }));
// passport middleware invoked on every request to ensure session contains passport.user object
app.use(passport.initialize());
// load seriliazed session user object to req.user
app.use(passport.session());
// Only during the authentication to specify what user information should be stored in the session.
passport.serializeUser(function (user, done) {
console.log("Serializer : ", user);
done(null, user);
});
// Invoked on every request by passport.session
passport.deserializeUser(function (user, done) {
let validUser = userList.filter(user => user.userId === user.userId)[0];
console.log("D-serializer : ", validUser);
done(null,validUser);
});
// passport strategy : Only invoked on the route which uses the passport.authenticate middleware.
passport.use(new LocalStrategy({
usernameField: 'name',
passwordField: 'password'
},
function (username, password, done) {
console.log("Strategy : Authenticating if user is valid :", username)
let user = userList.filter(user => username === user.userName);
console.log("Valid user : ", user)
if (!user) {
return done(null, false, { message: 'Incorrect username.' });
}
return done(null, user[0]);
}
));
// intercept request for authentication
app.use(Server.isAuthenticated);
app.post('/services/login', passport.authenticate('local', {
successRedirect: '/profile',
failureRedirect: '/login'
}));
app.get('/services/logout', (req: any, res: any) => {
req.logout();
console.log("User Logout");
res.send("{status:'logout'}")
});
// http server creation
let server = http.createServer(app);
registerControllersFromFolder({ folderPath: './api' })
.then(router => {
app.use(router);
/* start express server */
})
.catch(err => {
/* error happened during loading and registering */
});
server.listen(7000, () => {
console.log('Up and running on port 7000');
});
}
}
exports.startServer = Server.startServer;
// Call a module's exported functions directly from the command line.
require('make-runnable');
When I hit localhost:7000 it serves the index.html page as I have used
app.use(express.static(__dirname + "/public"));
and this is an angular app and because of angular routing login module will get loaded by default. I have used a middleware that checks request authentication and if true then based on request prefix (angular or express) routing is done.
For the login request defined local strategy method is called and if this is true it calls serializer method that takes the responsibility which data should be stored in the request session. and then sucessRedirect or failureRedirect is called.
For subsequent request, As I have used middleware that checks if req.isAuthenticated is true if so then request is served otherwise the user is redirected to login page. I know in every subsequent request deserializeUser method is called that contains the object that was stored by serializeUser method in the login request. As per the document, this makes a call to the database to check valid user.
But I am confused but is the actual use case of deserializeUser method? Where can I take the benefit of this method and if I am intercepting ecah request and check req.isAuthenticted() then why to call database in deserializeUser method?>
As stated in this answer
The first argument of deserializeUser corresponds to the key of the
user object that was given to the done function (see 1.). So your
whole object is retrieved with help of that key. That key here is the
user id (key can be any key of the user object i.e. name,email etc).
In deserializeUser that key is matched with the in memory array /
database or any data resource.
The fetched object is attached to the request object as req.user
Thus, the benefit of deserializeUser is that you have the user object available on every request thereafter.
You ask why you need to use deserializeUser if you call req.isAuthenticated, and the answer lies in the implementation of req.isAuthenticated:
req.isAuthenticated = function() {
var property = 'user';
if (this._passport && this._passport.instance) {
property = this._passport.instance._userProperty || 'user';
}
return (this[property]) ? true : false;
};
To me, it looks like req.isAuthenticated is looking for req[user] to be set, and thus, deserializeUser must be called before it can work.

Passport middleware, check if the user already has a living session from

I am building a web application using angular-fullstack. The stack is using express-sessions for session storage (in Mongodb) and passport.js for authentication.
I want to limit each user to a single login session. I am trying find a way to check if a user already has a living session when they login.
Is there a way to programmatically call a route to query mongodb from the passport middleware?
'use strict';
import path from 'path';
import passport from 'passport';
import {Strategy as LocalStrategy} from 'passport-local';
import express from 'express';
import session from 'express-session';
import _ from 'lodash';
import Session from '../../api/session/session.model';
var app = express();
require('run-middleware')(app);
function localAuthenticate(User, email, password, done, req) {
User.findOne({
email: email.toLowerCase()
}).exec()
.then(user => {
if (!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
// HERE is where I am trying to check if a user
// already has a living session when they login
// I tried to use the runMiddleware
// to query mongodb for all the existing sessions
// but I get this error: http://pastebin.com/YTeu5AwA
app.runMiddleware('/sessions',{},function(code,data){
console.log(code) // 200
console.log(data) // { user: '20', name: 'Moyshale' }
});
// Is there a way to access and use an existing route?
user.authenticate(password, function(authError, authenticated) {
if (authError) {
return done(authError);
}
if (!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
} else {
return done(null, user);
}
});
})
.catch(err => done(err));
}
export function setup(User, config) {
passport.use(new LocalStrategy({
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password' // this is the virtual field on the model
}, function(req, email, password, done) {
return localAuthenticate(User, email, password, done, req);
}));
}
Ok, I figured it out and I'll try and explain what I did. My specific implementation required me to set up user 'seats', where each user is part of a group and each group is limited in N number of logins at a single time.
As I mentioned in the question, I am using the angular fullstack yeoman generator, so this solution is specific to that setup.
I created a 'sessions' API endpoint so that I could query and modify the sessions stored in the mongo db. I included a 'seat' record with type Number into the sessions model. This is used to keep track of the users seat status for each session. Each user is given a 'loginSeat' value which is used to populate this filed. Also the session now has a seatAllowed of type Boolean, true: the user is allowed to access the site, false: the user is not allowed access to the site.
'use strict';
import mongoose from 'mongoose';
var SessionSchema = new mongoose.Schema({
_id: String,
session: String,
expires: Date,
seat: Number,
seatAllowed: Boolean // true: the user is allowed to access the site, false: the user is not allowed access to the site
});
export default mongoose.model('Session', SessionSchema);
I modified server/auth/login/passport.js so that when a user logs into the site, all other users with a matching seat are bumped out.
'use strict';
import path from 'path';
import passport from 'passport';
import {Strategy as LocalStrategy} from 'passport-local';
import _ from 'lodash';
import Sessions from '../../api/session/session.model';
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
return updated.save()
.then(updated => {
return updated;
});
};
}
function localAuthenticate(User, email, password, done, req) {
User.findOne({
email: email.toLowerCase()
}).exec()
.then(user => {
if (!user) {
return done(null, false, {
message: 'This email is not registered.'
});
}
// When a user logs into the site we flag their seat as allowed
var updateSession = {'seat': user.loginSeat, 'seatAllowed': true};
Sessions.findById(req.session.id).exec()
.then(saveUpdates(updateSession))
// When a user logs into the site, we disallow the seats of all other sessions with matching seat
Sessions.find().exec()
.then(sessions => {
// Check for existing user logged in with matching login seat
for (var i = 0; i < sessions.length; i++) {
if (sessions[i].seat === user.loginSeat && sessions[i].id !== req.session.id) {
console.log('DISALOW SEAT:');
var updateSession = {'seatAllowed': false};
Sessions.findById(sessions[i].id).exec()
.then(saveUpdates(updateSession));
}
}
});
user.authenticate(password, function(authError, authenticated) {
if (authError) {
return done(authError);
}
if (!authenticated) {
return done(null, false, { message: 'This password is not correct.' });
} else {
return done(null, user);
}
});
})
.catch(err => done(err));
}
export function setup(User, config) {
passport.use(new LocalStrategy({
passReqToCallback: true,
usernameField: 'email',
passwordField: 'password' // this is the virtual field on the model
}, function(req, email, password, done) {
return localAuthenticate(User, email, password, done, req);
}));
}
Each time the client makes a request the isAuthenticated function is triggered. This is where I check for the seaAllowed boolean for the current session, if true, allow the user to access the site, otherwise logout the user:
function saveUpdates(updates) {
return function(entity) {
var updated = _.merge(entity, updates);
return updated.save()
.then(updated => {
return updated;
});
};
}
/**
* Attaches the user object to the request if authenticated
* Otherwise returns 403
*/
export function isAuthenticated() {
return compose()
// Validate jwt
.use(function(req, res, next) {
// Allow access_token to be passed through query parameter as well
if (req.query && req.query.hasOwnProperty('access_token')) {
req.headers.authorization = 'Bearer ' + req.query.access_token;
}
validateJwt(req, res, next);
})
// Attach user to request
.use(function(req, res, next) {
User.findById(req.user._id).exec()
.then(user => {
if (!user) {
return res.status(401).end();
}
req.user = user;
///////////////////////////
// Login seat limitation //
///////////////////////////
// Check if the user seat is allowed
Sessions.findById(req.session.id).exec()
.then(thisSession => {
// TODO access the session in a better way
if (thisSession.seatAllowed === false || thisSession.seatAllowed === undefined) {
res.redirect('/login');
}
})
next();
})
.catch(err => next(err));
});
}
Thats it.