How to update cookie containing jwt using passport authentication - node.js

I have a cookie containing a signed jwt that is valid for 5 mins. The jwt contains basic user info (for authentication), as well as a globally unique ID (guid). I store these guids in a database if they are valid, and on the next request after expiration of the jwt, I want to:
1.) Check the database for the guid and see that it is still valid (not blacklisted)
2.) Update the jwt within the cookie with a new 5minute validity and the same information
There are many errors I've run into, but nothing I've tried has worked, and I'm curious as to whether this is even possible or the correct approach at this point.
I am using the node js packages "passport-jwt" in conjunction with "jsonwebtoken" to create the jwts and verify them.
//////////////////////
//authorization.js
//////////////////////
const JWTStrategy = require('passport-jwt').Strategy;
const jwt = require('jsonwebtoken');
const mongoose = require('mongoose');
require('../models/Guids');
const Guids = mongoose.model('Guids');
module.exports.JWTStrategy = function (passport) {
passport.use('jwt', new JWTStrategy({
jwtFromRequest: req => cookieExtractor(req, 'token'),
secretOrKey: 'secret',
passReqToCallback: true
},
(req, jwt_payload, done) => {
if (Date.now() / 1000 > jwt_payload.exp) {
Guids.findOne({ _id: jwt_payload.guid, userId: jwt_payload.uid })
.then(guid => {
if (guid.valid) {
//REFRESH TOKEN HERE
//????????return done(null, jwt_payload);
} else {
//FORCE USER TO RE-AUTHENTICATE
//???????return done('access token expired');
}
})
.catch(err => {
console.log(err);
return done('failed to validate user');
});
} else {
return done(null, jwt_payload);
}
}
));
};
var cookieExtractor = function (req, tokenName) {
var token = null;
if (req && req.cookies) {
token = req.cookies[tokenName];
} else {
console.log('no cookie found');
}
return token;
};
--
/////////////////////////////
//app.js
/////////////////////////////
const express = require('express');
const cookieParser = require('cookie-parser');
const bodyParser = require('body-parser');
const passport = require('passport');
const jwt = require('jsonwebtoken');
const app = express();
require('./authorization').JWTStrategy(passport);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(cookieParser());
//Protected Route
app.get('/xyz', passport.authenticate('jwt', {session: false}), (req, res) => {
//route logic....
});
//login Route (creates token)
app.post('/login', (req, res, next) => {
payload = {
guid: 12345678901010101',
uid: '123456789',
};
req.login(payload, { session: false }, (err) => {
if (err) {
console.log(err);
} else {
const token = jwt.sign(payload, 'secret', {expiresIn: '30s'});
res.cookie('token', token, { httpOnly: true });
res.redirect('/xyz');
};
}
}
const port = process.env.PORT || 5000;
const server = app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
By default, when the token expires, the authenticate middleware on the protected route immediately throws a failure.
I would like to bypass this failure and instead execute some code in the "authorization.js" file where the comment says "REFRESH TOKEN HERE".
That line of code is never even reached because of the automatic failure! I've tried console logging before and after expiration.
I have even manually bypassed the automatic failure before, but the response object (res) which contains the cookies is not available in the passport-jwt strategy. I am a little lost on where this logic should be implemented if the designated spot is nonsensical due to it being a middleware function.
Additionally, if the protected route is a POST, and the token expired after the page was "GET"ted successfully, I would like to not impede the POST method. I would like to seamlessly refresh the token, and then move along with the POST.

Passport provides support for custom messages or to be more precise for custom callbacks. You have to call the passport authenticate middleware manually by yourself embedded in your own wrapper middleware. This allows access to the req and res objects. For more details see the documentation.
If the built-in options are not sufficient for handling an authentication request, a custom callback can be provided to allow the application to handle success or failure.
app.get('/login', function(req, res, next) {
passport.authenticate('local', function(err, user, info) {
if (err) { return next(err); }
if (!user) { return res.redirect('/login'); }
req.logIn(user, function(err) {
if (err) { return next(err); }
return res.redirect('/users/' + user.username);
});
})(req, res, next);
});

Related

Node.js, Express, Passport.js - req.user undefined after login

I am aware that there are there are loads of questions already on this particular topic and I have gone through all of them and none of the solutions have help so I am aware that this may be a local issue or something very obvious that I'm missing, however any help or tips that anyone could provide would be much appreciated.
I am using passport.js to authenticate users in my express app. The authentication doesn't seem to raise any errors and seems to execute correctly as the users are stored in the session data in the db, e.g.
session: {"cookie":{"originalMaxAge":3600000,"expires":"2023-01-18T15:00:13.646Z","secure":false,"httpOnly":true,"path":"/"},"passport":{"user":"63c69e118d89ef5921231f75"}}
Despite this, if I try to access req.user in any of the controller modules, it appears undefined.
I will paste my code below:
app.js
// Declare imported packages and modules
const express = require('express');
const MongoStore = require('connect-mongo');
const passport = require('passport');
const methodOverride = require('method-override');
const session = require('express-session');
const staticDirectory = require('serve-static');
require('dotenv').config({ path: './config/config.env' });
const dbConnect = require('./config/db');
const authRouter = require('./routes/authRoutes');
const moviesRouter = require('./routes/moviesRoutes');
const userRouter = require('./routes/userRoutes');
const errorHandler = require('./middleware/errorHandler');
// Declare variables for server
const HOSTNAME = process.env.HOST || 'localhost';
const PORT = process.env.PORT || 3000;
// Instantiate express app
const app = express();
// Add middleware to instantiated app
app.use(express.json());
app.use(staticDirectory(__dirname + '/public'));
app.use(methodOverride('_method')); // TODO: read about method override
// Add session middleware and declare session parameters
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: true,
cookie: { maxAge: 1000 * 60 * 60 },
store: MongoStore.create({
mongoUrl: `${process.env.DB_SERVER}/${process.env.database}`,
collection: 'sessions'
})
}));
// Add passport middleware
app.use(passport.initialize());
app.use(passport.session());
// Add routes to defined endpoints
app.use('/auth', authRouter);
app.use('/user/favourites', userRouter);
app.use('/', moviesRouter);
// Add error-handling middleware
app.use(errorHandler);
// Connect the db
dbConnect();
// Run the server
app.listen({ path: HOSTNAME, port: PORT }, (error) => {
if (error) return console.log(error);
console.log(`Server is running on port ${PORT}...`);
});
passport-config.js
onst LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
const User = require('../models/User');
// Middleware function to initialise passport
const initialize = (passport) => {
// Defining the fields used to login
const customFields = {
usernameField: 'email',
passwordField: 'password'
};
// Function to determin how authentication is handled
const authenticateUser = async (email, password, done) => {
// Finding user in the db
const user = await User.findOne({ email: email });
if (!user) {
// No user found returns an error
return done(null, false, { message: `No user with email ${email} found` });
}
try {
// Checking password entered for user hashed against the stored hash
if (await bcrypt.compare(password, user.hash)) {
return done(null, user);
} else {
// return an error if the hashes don't match
return done(null, false, { message: 'Password incorrect' });
}
} catch (error) {
return done(error);
}
}
passport.use(new LocalStrategy(customFields, authenticateUser));
// Built-in passport.js method to add user id in session cookie info
passport.serializeUser((user, done) => {
return done(null, user.id);
});
// Built-in passport.js method to extract user id from session to obtain user info
passport.deserializeUser(async (id, done) => {
const user = await User.findOne({ _id: id });
return done(null, user);
})
}
module.exports = initialize;
In my authControllers file, I have tried a couple of options:
exports.login = (req, res, next) => {
try {
// Login using passport authenticate function
passport.authenticate('local', {
successRedirect: '/',
failureRedirect: '/'
})(req, res, next);
} catch (error) {
next(error);
}
}
and
exports.login = (req, res, next) => {
try {
// Login using passport authenticate function
passport.authenticate('local', (error, user, info) => {
if (error) return next(error);
if (!user) return res.redirect('/');
req.login(user, (err) => {
if (err) return next(err);
res.redirect('/');
})
})(req, res, next);
} catch (error) {
next(error);
}
}
I have also tried importing cors module and adding
app.use(
cors({
origin: `http://localhost:${process.env.PORT}`,
credentials: true,
})
);
and also adding
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Allow-Credentials", true);
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE');
res.header('Access-Control-Allow-Headers', 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept');
if ('OPTIONS' == req.method) {
return res.send(200);
}
next();
});
I have also tried adding { session: true } to the options for passport.authenticate(), switching from express-session to cookie-session, adding bodyParser middleware, adding cookieParser middleware. I checked the order of app.use(session({...})) and the passport functions and tried removing some of the middleware to see if it was interfering. I also tried manually saving the user in the login function and adding the user to res.locals and without results. And also importing passport-config.js after the passport functions:
app.use(passport.initialize());
app.use(passport.session());
const initializePassport = require('./config/passport-config')(passport);
All of which without results.
Sorry for the long question and as I said, any insight on what is going wrong here would be much appreciated. Thanks!

JWT verification for Swagger NodeJs API middleware

I generated with Swagger a Node js API. I'm trying to add a jwt token check in my API to allow access to protected ressources. I would like to use something similar to passport js but I don't understand where I need to add the "passport.authenticate" method.
This is the way the app is initialized:
function initializeApp(swaggerOption, swaggerDoc) {
const p = new Promise((resolve, reject) => {
swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) {
// Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain
app.use(middleware.swaggerMetadata());
// Validate Swagger requests
app.use(middleware.swaggerValidator());
// Route validated requests to appropriate controller
app.use(middleware.swaggerRouter(swaggerOption));
// Serve the Swagger documents and Swagger UI
app.use(middleware.swaggerUi());
resolve(app);
})
});
return p;
}
exports.initializeApp = initializeApp;
initializeApp(options, swaggerDoc ).then((app) => {
// Start the server
http.createServer(app).listen(serverPort, function () {
console.log('Your server is listening on port %d (http://localhost:%d)', serverPort, serverPort);
console.log('Swagger-ui is available on http://localhost:%d/docs', serverPort);
});
})
And an example of a controller
'use strict';
var utils = require('../utils/writer');
var Auth = require('../service/AuthService');
module.exports.authenticatePUT = function authenticatePUT (req, res, next) {
var body = req.swagger.params['body'].value;
Auth.authenticatePUT(body)
.then(function (response) {
utils.writeJson(res, response);
})
.catch(function (response) {
utils.writeJson(res, response);
});
}
Since there is no route like so I can siply follow passport js's syntax
app.get ('/ profile',
passport.authenticate ('bearer', {session: false}),
function (req, res) {
res.json (req.user);
});
and no documentation (or I don't find) for middleware methods I don't find where I can add the jw token verification. If someone has some examples or explanations I would be verry happy :)
Thanks !
I am not using passport, but a simple example is like so:
In your routes the middleware is defined just as a function to run before proceeding further. It will implicitly receive the request data.
app.post(
'/api/object/create',
auth.getToken, // << middleware evaluate
objectController.create
)
I have created my own middleware evaluation, it looks like this:
getToken: function (req, res, next) {
const bearerHeader = req.headers['authorization']
// Get the bearer token from the request
if (typeof bearerHeader !== 'undefined') {
const bearer = bearerHeader.split(' ')
const bearerToken = bearer[1]
req.token = bearerToken
next()
} else {
res.sendStatus(401)
}
}
Now, this only checks to see if we have a token at all, it is a pretty simple check and is not in any way a security check.
This is because we may want to do more checks at the objectController.create function, about who this is, and what they should be able to do next. These checks could also be done within the above function, depending on your use case.
objectController.create receives the request, and the first thing it does is check the token
const currentUser = await currentUser(req.token)
which checks if this user is someone in the database, based on decoding from the jwt signing secret
async currentUser(token) {
const decoded = jwt.verify(token, process.env.USER_AUTH_SECRET) // << very important never to commit this as a readable value in your repo, store in a local environment variable
const user = await User.findOne({
where: { email: decoded.user.email },
})
console.log(user.email)
return user
},
So, whilst this is not a Passport solution, it hopefully shows the basic process of middleware and then authenticating a jwt, which I think was your general question.
I think you want something like this:
const fs = require('fs');
const swaggerTools = require('swagger-tools');
const path = require('path');
const jsYaml = require('js-yaml');
const passport = require('passport');
const cors = require('cors');
const CONSTANTS = require('./constants');
module.exports = function initializeSwagger(app) {
// swaggerRouter configuration
const swaggerRouterOptions = {
swaggerUi: path.join(__dirname, '/swagger.json'),
controllers: path.join(__dirname, '../controllers'),
useStubs: process.env.NODE_ENV === 'development' // Conditionally turn on stubs (mock mode)
};
const swaggerDoc = jsYaml.safeLoad(fs.readFileSync(path.join(__dirname, '../api/swagger.yaml'), 'utf8'));
// Initialize the Swagger middleware
swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) {
// Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain
app.use(middleware.swaggerMetadata());
// *** Where you will call your JWT security middleware ***
app.use(initializeSwaggerSecurity(middleware));
//enable CORS
app.use(cors());
// Validate Swagger requests
app.use(middleware.swaggerValidator());
// Route validated requests to appropriate controller
app.use(middleware.swaggerRouter(swaggerRouterOptions));
// Serve the Swagger documents and Swagger UI
if(process.env.NODE_ENV === "prod"){
app.use('/docs', (req, res, next) => {
});
} else {
app.use(middleware.swaggerUi());
}
});
};
function initializeSwaggerSecurity(middleware) {
return middleware.swaggerSecurity({
jwtAuth: (req, authOrSecDef, scopes, callback) => {
passport.authenticate('jwt', {session: false}, (err, user, info) => {
if (err) {
return callback(new Error(CONSTANTS.AUTHENTICATION.ERROR_MESSAGE_DEFAULT))
};
if (!user) {
// no user session, were we tampered? What happened?
// #param info has that detail!
// console.log('url requested: ' + req.url + ' | raw headers: ' + req.rawHeaders);
// console.log('api: passport => jwt fn() initializeSwaggerSecurity(), rejected jwt token, token tampered or user session does not exist; failed to authenticate token: ', info);
return callback(new Error(CONSTANTS.AUTHENTICATION.ERROR_MESSAGE_TOKEN))
}
else {
req.user = user;
return callback();
}
})(req, null, callback);
}
});
};
Hope this helps you or anybody else who might be looking to implement this on Swagger 2.0

Is it safe to store user data in the localStorage through Angular?

I am new to the world of Angular and i am trying to build my first Angular app, at the moment i do not understand if it is safe to store user data in the localStorage, if not, what should i do to save it? i am using cookie sessions.
This is how my backend code works (only the Authentication and Authorization part here):
const express = require('express');
const session = require('cookie-session');
const crypt = require('bcryptjs');
const config = require('../config');
const router = express.Router();
const users = require('../models/users');
const app = express();
app.use(session({
name: 'session',
keys: config.keys,
secret: config.sessionkey,
cookie: {
secure: false,
httpOnly: true,
path: 'cookie',
expires: new Date(Date.now() + 60 * 60 * 1000 * 24 * 365)
}
}));
router.get('/', (req,res)=>{
if(isAuthenticated(req)){
res.send(req.session.user);
} else {
res.send({"message":"Authenticate first."});
}
});
router.post("/login", (req, res) => {
const email = req.body.email;
const userpass = req.body.userpass;
users.findUserByEmail(email,function(user){
if(crypt.compareSync(userpass,user[0].userpass)){
req.session.user = user;
res.send(user);
}
});
});
router.post("/logout", (req,res)=> {
req.session = null;
res.redirect("/");
});
router.post("/register", (req, res) => {
var user = { "username": req.body.username, "userpass": crypt.hashSync(req.body.userpass, 10) , "email": req.body.email, "birthDate": req.body.birthDate };
users.createUser(user,(err)=>{
if(!err){
res.send({ "message":"Login" });
} else {
res.send({ "message":"Something went wrong during registration please retry." });
}
});
});
router.post("/unregister", isAuthenticated, (req, res) => {
var email = req.body.email;
if(isAuthenticated(req)){
users.findUserByEmail(email,function(user){
if(req.session.user == user){
if(crypt.compareSync(req.body.userpass,user[0].userpass)){
req.session = null;
users.deleteUserByEmail(email,function(err){
if(err){
res.send({ "message":"Something went wrong while deleting the user please retry." });
} else {
res.send({ "message":"User deleted successfully" });
}
});
}
}
});
} else {
res.send({ "message":"Authenticat first." });
}
});
function isAuthenticated(req){
return req.session.user != null;
}
module.exports.app = app;
module.exports.Router = router;
module.exports.isAuthenticated = isAuthenticated;
As you can see when the use logs in correctly i just send him the user data stored in the db as it is (obviously without the userpass), so what should i do to make a secure session in the angular side?
You should use JWT to exchange authentication data between server and client if you're new the idea of JWT, you may read a little about it in the intro, using JWT you will have the ability to read it in the client (Angular) and save it to localStorage, and no worries about its security because it's a hash after all and even if anyone could read its contents he/she won't be able to modify it, because it could only be generated identically by your security key/signature in your server.
Of course, you'll have to send the jwt token with every request to your server, and in Angular, it's very easy to use interceptors to achieve such an approach.
Here's a complete example demonstrating the usage of JWT with Angular and NodeJS.

Node.js - JWT, API: How To Implement Multiple Passport.js Authentication Middleware?

So I am trying to build two JWT authentication systems for one app. One for the users and one for businesses. The authentication uses passport (local and JWT). I started working on the user aspect of the authentication and I was able to make it work as I wanted. I thought it would be the same for business. All I did was simply copy paste and change few variable. Normally the business endpoints should work but it does not. So I tried to debug what was going on and I am still trying to figure out what it is. My guess is that when I load the strategies in my app.js, only the passport strategies for users get called. If I permute positions between passport and business-passport, everything breaks.
Here is my code.
Passport Middleware For User passport.js
const passport = require('passport');
const JwtStrategy = require('passport-jwt').Strategy;
const {ExtractJwt} = require('passport-jwt');
const LocalStrategy = require('passport-local').Strategy;
const { JWT_SECRET } = require('./config');
const User = require('./models/user');
// const Business = require('./models/business');
passport.use(new JwtStrategy({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: JWT_SECRET,
passReqToCallback: true
}, async (req, payload, done) => {
console.log('payload', payload)
try {
// Find the user specified in token
const user = await User.findById(payload.sub);
// Handle if User dont exist
if (!user) {
return done(null, false);
}
// Otherwise, return the user
req.user = user;
done(null, user);
} catch (error) {
done(error, false);
}
}));
// Local Strategy
passport.use(new LocalStrategy({
usernameField: 'email'
}, async (email, password, done) => {
try {
// Find the user given the email
const user = await User.findOne({email});
// If not, handle it
if (!user) {
return done(null, false);
}
// check if password is correct
const isMatch = await user.isValidPassword(password);
// if not, handle it
if (!isMatch) {
return done(null, false);
}
// Otherwise, return the user
done(null, user);
} catch (error) {
done(error, false);
}
}));
Passport Middleware For Business passport-business.js
Just change User for Business
Controllers for Users controllers/user.js
const fs = require('fs');
const mongoose = require('mongoose');
const JWT = require('jsonwebtoken');
const { JWT_SECRET } = require('../config');
const User = require('../models/user');
signToken = user => {
// Respond with token
return JWT.sign({
iss: 'My Business, Inc.',
userType: 'users',
sub: user._id, /* You can also use newUser.id */
iat: new Date().getTime(), /* Current Time */
exp: new Date().setDate(new Date().getDate() + 1), /* Current Time + 1 day ahead */
}, JWT_SECRET);
};
module.exports = {
signup: async (req, res, next) => {
console.log('req.value.body', req.value.body);
const {email, password} = req.value.body;
// Check if User already exist by email address
const foundUser = await User.findOne({email});
if(foundUser) {
return res.status(403).json({error: 'Email is already in use'});
}
// Create a new User
const newUser = new User({email, password});
await newUser.save();
// Generate the token
const token = signToken(newUser);
// Respond with token
res.status(200).json({token});
},
signin: async (req, res, next) => {
// Generate token
const token = signToken(req.user);
res.status(200).json({token});
// console.log('signin');
},
};
The reason why I have req.value... is because of a middleware that I used to validate my body and url params.
Controllers for Business controllers/business.js .
Everything is the same, just change User with Business.
I should also point out that whenever I use jwt.io to see if my token signature is valid, it always says invalid signature, but everything works on my application as expected. Wondering why it is saying invalid signature.
Routes for Users routes/user.js . Please remember that it is the same for Business.
const express = require('express');
// this router deals better with "try{} catch{}" situations"
const router = require('express-promise-router')();
const passport = require('passport');
const UserController = require('../controllers/user');
require('../passport');
const { validateBody, schemas, validateParam } = require('../helpers/route-helpers');
const StrategyLocal = passport.authenticate('local', {session: false});
const StrategyJWT = passport.authenticate('jwt', {session: false});
router.get('/', (req, res) => {
res.json({message: "Welcome to our User's API Endpoint!"});
});
router.route('/signup')
// .get(UserController.index)
.post([validateBody(schemas.authSchema)], UserController.signup);
router.route('/signin')
// Makes sense to implement localstrat here because jWt will always work because the user was issued a token at signup
// So both solutions will work here as a strategy
.post(validateBody(schemas.authSchema), StrategyLocal, UserController.signin);
module.exports = router;
And finally, the file that I think is causing the problem... Introducing...
app.js
const express = require('express');
const logger = require('morgan');
const mongoose = require('mongoose');
const bodyParser = require('body-parser');
const passport = require('passport');
const path = require('path');
const cors = require('cors');
const { dbName } = require('./config');
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost/' + dbName, {
useNewUrlParser:true,
useUnifiedTopology:true,
useCreateIndex: true,
useFindAndModify: false
});
const app = express();
// 👇👇👇👇 This is where the problem is happening in my opinion
require('./passport-business')
require('./passport')
const user = require('./routes/user');
const business = require('./routes/business');
// Middlewares
// set the static files location /public/img will be /img for users
app.use(logger('dev'));
app.use(cors());
app.use(bodyParser.urlencoded({ extended: false}));
app.use(bodyParser.json());
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, noauth");
if (req.method === 'OPTIONS'){
res.header("Access-Control-Allow-Methods", "PUT, POST, PATCH, DELETE, OPTIONS");
res.setHeader('Access-Control-Allow-Credentials', false);
return res.status(200).json({});
}
next();
});
app.use('/api/users', user);
app.use('/api/businesses', business);
// Start the server
const port = app.get('port') || 2019;
app.listen(port, () => console.log(`Server is listening on port ${port}`));
If I keep everything as is, all User endpoints work, signup works for Business, but I get 401 when I try to signin (Business). But if I do this...
require('./passport')
require('./passport-business')
I get 401 when I try to signin (User) and I get 500 when I try to signin (Business).
Can someone tell me what I am doing wrong here? Maybe it is the order in which I load these files in app.js. Maybe it is the fact that I am creating two separate passport files for User and Business. I would love to know how to combine those two into one. .
So in passport.js you do this:
try {
// Find the user specified in token
const user = await User.findById(payload.sub);
// Handle if User dont exist
if (!user) {
return done(null, false);
}
// Otherwise, return the user
req.user = user;
done(null, user);
} catch (error) {
done(error, false);
}
Instead of cloning the file, couldn't you get userType that you're already signing into the JWT and make a conditional?
ie:
try {
// Find the user specified in token
let user = null;
if (payload.userType === 'user') {
user = await User.findById(payload.sub);
}
if (payload.userType === 'business') {
user = await Business.findById(payload.sub);
}
// Handle if User dont exist
if (!user) {
return done(null, false);
}
// Otherwise, return the user
req.user = user;
done(null, user);
} catch (error) {
done(error, false);
}
Then were you require two passport files, you'd require just the one

Authorization in a NodeJS Express REST server

I am building a REST server in nodejs using express.
I would like to allow certain users to perform certain calls.
i.e. have an admin who can edit other users and see reports, where a user can only perform simple actions.
I was trying to use passport.js and passport-ldapauth, and also I would like to perform different queries for authentication (check credentials) and authorization (check if the user is part of a group).
var fs = require('fs');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var passport = require('passport');
var LdapStrategy = require('passport-ldapauth');
var app = express();
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Allow self signed certificates
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
app.use('/', index);
app.use('/users', users);
var OPTS = {
server: {
url: 'ldaps://...',
bindDN: 'cn=Manager,dc=com',
bindCredentials: 'secret',
searchBase: 'ou=people,dc=com',
searchFilter: '(uid={{username}})',
tlsOptions: {
ca: [fs.readFileSync('/path/to/certificate.crt')]
}
},
handleErrorsAsFailures: true,
failureErrorCallback: (err) => console.log(err)
};
passport.use(new LdapStrategy(OPTS));
passport.use('test', new LdapStrategy(OPTS));
app.use(passport.initialize());
app.post('/login', function(req, res, next) {
passport.authenticate('ldapauth', function(err, user, info) {
if (err) return next(err);
if (!user) return res.status(401).send(info);
res.json(user);
// req.logIn(user, function(err) {
// if (err)
// console.error(err);
// if (err) return next(err);
// return res.json(user);
// })
})(req, res, next);
});
The passport-ldapauth strategy does not allow you to perform any additional checks or queries as far as I know from reading over the documentation. The strategy and Passport in general is aimed at making the login/authentication process seamless and easy as possible. So any additional constraints will need to be handled on your own.
With that said, passport-ldapauth utilizes ldapauth-fork underneath which in turn uses ldapjs. You can try to utilize ldapjs as shown here and here, but I think the easiest solution would be to use ldapauth-fork directly.
We first need to set up ldapauth-fork, so we'll use the following example app/ldap/index.js:
const LdapAuth = require('ldapauth-fork')
const ldap = new LdapAuth({
url: 'ldaps://...',
bindDN: 'cn=Manager,dc=com',
bindCredentials: 'secret',
searchBase: 'ou=people,dc=com',
searchFilter: '(uid={{username}})',
tlsOptions: {
ca: [fs.readFileSync('/path/to/certificate.crt')]
})
ldap.on('error', (err) => { throw err })
module.exports = ldap
Our example app/controllers/auth.js could look something like this:
const jwt = require('jsonwebtoken')
const ldap = require('../ldap')
const { User } = require('../database/models') // mongoose model
const Promise = require('bluebird')
exports.login = async (req, res) => {
const { username, password } = req.body
if (!username || !password) {
res.status(400
res.json({ error: 'Missing username or password.' })
return
}
// ldapauth-fork doesn't support Promises.
// You can try to promisfy it, but I prefer this.
// I've named it `profile`, but you can name it whatever you want.
const profile = await Promise.fromCallback(cb => ldap.authenticate(username, password, cb))
// Since this is a REST API, we need to send back a token.
// For this example, we're creating it by hand.
const token = jwt.sign({ user: profile }, 'secret', {})
// Use epoch time from the token instead of generating it ourselves.
const { exp } = jwt.verify(token, 'secret')
// Finally send the token.
// By convention, the keys are snake case.
res.json({
access_token: token,
token_type: 'Bearer',
expires_in: exp,
user: profile
})
}
Now that we have our token created, we need a way to verify that token. To do that we need to write our own middleware. So for example app/middleware/valid-token.js:
const jwt = require('jsonwebtoken')
exports.needsAdminAccess = (req, res, next) => {
// This token should have already been validated by the `requiresToken` middleware
let token = req.header('authorization').split(' ')[1]
token = jwt.verify(token, 'secret')
// Let's check if they are in the admin group
// Remember that we set the user/profile value in the controller.
if (!token.user.dn.includes('ou=ADMIN')) {
next(new Error('You must be an admin to access this route.'))
return
}
// Any additional checks would go here.
// ...
// If everything is fine then call next to let the request continue.
next()
}
exports.requiresToken = (req, res, next) => {
// Assuming the token is in the header as Authorization: Bearer token
let token = req.header('authorization').split(' ')[1]
// Make sure our secret key matches
token = jwt.verify(token, 'secret')
// Additional checks of the token should be done here as well.
// ...
// Don't forget to call next if all is good
next()
}
Finally we use the middleware wherever you define your routes, for example:
const express = require('express')
const app = express()
const { requiresToken, needsAdminAccess } = require('./middleware/valid-token')
// This route needs a valid token, but not admin rights
app.get('/user', requiresToken, (req, res) => { })
// This route needs a valid token AND admin rights
app.get('/admin', requiresToken, needsAdminAccess, (req, res) => { })
I wrote everything from scratch to hopefully paint a clear picture on how everything works. You can use other packages to validate the token for you, but we need to do verify specific things so we write our own.

Resources