Skip to content

Instantly share code, notes, and snippets.

@dharapvj
Forked from phillipsmith/auth-cache.js
Created February 5, 2019 11:05
Show Gist options
  • Save dharapvj/b75fa37b21d803bd0306def4bf38ac69 to your computer and use it in GitHub Desktop.
Save dharapvj/b75fa37b21d803bd0306def4bf38ac69 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));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment