Skip to content

Instantly share code, notes, and snippets.

@phillipsmith
Last active May 11, 2020 01:38
Show Gist options
  • Save phillipsmith/be39c3af5f48451379d5a9ec9cb05228 to your computer and use it in GitHub Desktop.
Save phillipsmith/be39c3af5f48451379d5a9ec9cb05228 to your computer and use it in GitHub Desktop.
Express.js + Passport.js: LDAP Basic Authentication for Login and Bearer Token Authentication for everything else
/**
* Copyright (c) 2017, Three Pawns, Inc. All rights reserved.
*/
'use strict';
const config = require('config');
const crypto = require('crypto');
const uuid = require('uuid');
const NodeCache = require('node-cache');
const algorithm = 'aes-256-ctr';
const password = config.get('servers.auth.crypt.password');
const cacheConfig = config.get('servers.auth.cache');
const ttl = cacheConfig.ttl;
const check = cacheConfig.check;
const encrypt = (text) => {
const cipher = crypto.createCipher(algorithm, password);
return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
};
const decrypt = (text) => {
const decipher = crypto.createDecipher(algorithm, password);
return decipher.update(text, 'hex', 'utf8') + decipher.final('utf8');
};
const userCache = new NodeCache({
stdTTL: ttl,
checkperiod: check,
});
/**
* Process the authorization: Basic header
*/
module.exports.basicLDAP = function basicLDAP(passport) {
return (req, res, next) => {
const authorization = req.get('Authorization');
if (!authorization) {
// Send the request for Basic Authentication and exit
res.set('WWW-Authenticate', 'Basic').status(401).json({
message: 'Missing header',
});
return next();
}
// Do the authentication
return passport.authenticate('ldapauth', (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// Authentication failed
return res.set('WWW-Authenticate', 'Basic').status(401).json(info || {});
}
// Authentication succeeded so login the user
req.user = user; // req.logIn() is not called because no session is needed
// Encrypt the header because it contains the password
const basic = encrypt(authorization);
// Create the bearer token and cache the basic header
const token = new Buffer(uuid.v4()).toString('base64');
userCache.set(token, basic);
// Put the token in the response and send
const reply = info || {};
reply.token = token;
return res.status(200).json(reply);
})(req, res, next);
};
};
/**
* Process the authorization: Bearer header
*/
module.exports.bearer = function bearer(passport) {
return (req, res, next) => {
const authorization = req.get('Authorization');
const token = authorization ? authorization.match(/bearer\s+([\S]+)$/i) || [] : [];
const cached = token[1] ? userCache.get(token[1]) : undefined;
if (!authorization) {
res.redirect('/login');
return next();
}
if (!cached) {
// Either basic authentication -or- an expired or invalid bearer authentication
res.set('WWW-Authenticate', 'Bearer').status(401).json({
message: 'Login to get a new bearer token',
});
return next();
}
// Decrypt the cached basic authorization header and override the header
const basic = decrypt(cached);
req.headers.authorization = basic;
// Do the authentication with the overridden header
return passport.authenticate('ldapauth', (err, user) => {
if (err) {
return next(err);
}
if (!user) {
// Authentication failed
return res.redirect('/login');
}
// Authentication succeeded so login the user
req.user = user; // req.logIn() is not called because no session is needed
return next();
})(req, res, next);
};
};
/**
* Copyright (c) 2017, Three Pawns, Inc. All rights reserved.
*/
'use strict';
const config = require('config');
const jwt = require('jsonwebtoken');
const secret = config.get('servers.auth.token.secret');
const ttl = config.get('servers.auth.token.ttl');
const encrypt = (text, done) => {
jwt.sign({
token: text,
}, secret, {
expiresIn: ttl,
}, (err, token) => {
if (err) {
done(err);
} else {
done(undefined, token);
}
});
};
const decrypt = (text, done) => {
jwt.verify(text, secret, (err, decoded) => {
if (err) {
done(err);
} else {
done(undefined, decoded);
}
});
};
/**
* Process the authorization: Basic header
*/
module.exports.basicLDAP = function basicLDAP(passport) {
const send401 = (msg, req, res, next) => {
res.set('WWW-Authenticate', 'Basic').status(401).json(msg || {});
next();
};
return (req, res, next) => {
const authorization = req.get('Authorization');
if (!authorization) {
// Send the request for Basic Authentication and exit
return send401({
message: 'Missing header',
}, req, res, next);
}
// Do the authentication
return passport.authenticate('ldapauth', (err, user, info) => {
if (err) {
return next(err);
}
if (!user) {
// Authentication failed
return send401(info, req, res, next);
}
// Authentication succeeded so login the user
req.user = user; // req.logIn() is not called because no session is needed
// Encrypt the header because it contains the password
return encrypt(authorization, (error, basic) => {
if (error) {
next(error);
} else {
// Create the bearer token
const base64 = new Buffer(basic).toString('base64');
// Put the token in the response and send
const reply = info || {};
reply.token = base64;
res.status(200).json(reply);
}
});
})(req, res, next);
};
};
/**
* Process the authorization: Bearer header
*/
module.exports.bearer = function bearer(passport) {
const send401 = (req, res, next) => {
res.set('WWW-Authenticate', 'Bearer').status(401).json({
message: 'Login to get a new bearer token',
});
next();
};
return (req, res, next) => {
const authorization = req.get('Authorization');
const key = authorization ? authorization.match(/bearer\s+([\S]+)$/i) || [] : [];
if (!authorization) {
res.redirect('/login');
return next();
}
if (!key[1]) {
// Invalid bearer authentication
return send401(req, res, next);
}
// Validate token is still valid and decrypt the basic authorization header
const token = new Buffer(key[1], 'base64').toString('UTF-8');
return decrypt(token, (err, basic) => {
if (err) {
return (err.name === 'TokenExpiredError') ? send401(req, res, next) : next(err);
}
// Override the authorization header
req.headers.authorization = basic.token;
// Do the authentication with the overridden header
return passport.authenticate('ldapauth', (error, user) => {
if (error) {
next(error);
} else if (!user) {
// Authentication failed
res.redirect('/login');
} else {
// Authentication succeeded so login the user
req.user = user; // req.logIn() is not called because no session is needed
next();
}
})(req, res, next);
});
};
};
const express = require('express');
const passport = require('passport');
const basicAuth = require('basic-auth');
const LdapStrategy = require('passport-ldapauth');
const auth = require('./auth.js');
const ldapOptions = {/* load from config */};
ldapOptions.credentialsLookup = basicAuth;
passport.use(new LdapStrategy(ldapOptions, (user, done) => {
const extractor = /cn[=]([^,]+)/; // extract cn and set user.groups
const memberOf = user.memberOf || user.isMemberOf || [];
user.groups = (Array.isArray(memberOf) ? memberOf : [memberOf]).map(dn => dn.match(extractor)[1]);
done(null, user);
}));
const app = express();
app.use(passport.initialize());
// Configure Basic Authentication - LDAP (/login)
app.use(/^[/]login[/]?/, auth.basicLDAP(passport));
// Configure Bearer Authentication - Cached (not /login)
app.use(/^[/](?!login)/, auth.bearer(passport));
@phillipsmith
Copy link
Author

Can you mention the license under which the above code is released?

This code is licensed under MIT

@JoyceBabu
Copy link

This code is licensed under MIT

Thank you for the clarification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment