Last active
May 11, 2020 01:38
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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); | |
}); | |
}; | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); |
Can you mention the license under which the above code is released?
This code is licensed under MIT
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
Can you mention the license under which the above code is released?