Reputation: 1595
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);
});
Upvotes: 1
Views: 1709
Reputation: 23070
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.
Upvotes: 1