UNPKG

passport-atlassian-crowd2

Version:

Password authentication strategy using Atlassian Crowd for Passport

300 lines (264 loc) 10.2 kB
/** * Module dependencies. */ var passport = require('passport-strategy'), https = require('https'), http = require('http'), URL = require('url'), util = require('util'), BadRequestError = require('./errors/badrequesterror'), HttpsProxyAgent = require('https-proxy-agent'), HttpProxyAgent = require('http-proxy-agent'); /** * `Strategy` constructor. * * The local authentication strategy authenticates requests based on the * credentials submitted through an HTML-based login form. * * Applications must supply a `verify` callback which accepts `username` and * `password` credentials, and then calls the `done` callback supplying a * `user`, which should be set to `false` if the credentials are not valid. * If an exception occured, `err` should be set. * * Optionally, `options` can be used to change the fields in which the * credentials are found. * * Options: * - `crowdServer` the URL of the Crowd server * - `crowdApplication` the name of the application in Crowd * - `crowdApplicationPassword` the password for the application in Crowd * - `usernameField` field name where the username is found, defaults to _username_ * - `passwordField` field name where the password is found, defaults to _password_ * - `retrieveGroupMemberships` if `true`, retrieve group information about users * - `requestOptions` additional options to provide when making requests; currently * only _ca_ is supported * * Examples: * * passport.use(new LocalStrategy( * function(username, password, done) { * User.findOne({ username: username, password: password }, function (err, user) { * done(err, user); * }); * } * )); * * @param {Object} options * @param {Function} verify * @api public */ function Strategy(options, verify) { if (typeof options == 'function') { verify = options; options = {}; } if (!verify) throw new Error('atlassian-crowd authentication strategy requires a verify function'); if (!options.crowdServer) { throw new Error("atlassian-crowd strategy requires a crowd server url"); } this._crowdServer = options.crowdServer; this._crowdApplication = options.crowdApplication; this._crowdApplicationPassword = options.crowdApplicationPassword; var uri = URL.parse(options.crowdServer); this._crowdProtocol = uri.protocol; this._ignoreSSL = options.ignoreSSL; this._proxyURL = options.proxyURL; this._timeoutMs = "crowdRequestTimeout" in options && options.crowdRequestTimeout ? parseInt(options.crowdRequestTimeout) : 5000; this._usernameField = options.usernameField || 'username'; this._passwordField = options.passwordField || 'password'; passport.Strategy.call(this); this.name = 'atlassian-crowd'; this._verify = verify; this._retrieveGroupMemberships = options.retrieveGroupMemberships; this._requestOptions = options.requestOptions || {}; } /** * Inherit from `passport.Strategy`. */ util.inherits(Strategy, passport.Strategy); function _parseProfile(crowdUser) { return { provider:'atlassian-crowd', id:crowdUser.name, username:crowdUser.name, displayName:crowdUser["display-name"], name:{ familyName:crowdUser["last-name"], givenName:crowdUser["first-name"] }, email:crowdUser.email, emails:[ {value:crowdUser.email} ], _json:crowdUser }; } function _lookup(obj, field) { if (!obj) { return null; } var chain = field.split(']').join('').split('['); for (var i = 0, len = chain.length; i < len; i++) { var prop = obj[chain[i]]; if (typeof(prop) === 'undefined') { return null; } if (typeof(prop) !== 'object') { return prop; } obj = prop; } return null; } function _handleResponse(response, callback) { var result = ""; response.on("data", function (chunk) { result += chunk; }); response.on("end", function () { callback(response, result); }); } /** * Authenticate request based on the contents of a form submission. * * @param {Object} req * @api protected */ Strategy.prototype.authenticate = function (req, options) { options = options || {}; var username = _lookup(req.body, this._usernameField) || _lookup(req.query, this._usernameField); var password = _lookup(req.body, this._passwordField) || _lookup(req.query, this._passwordField); if (!username || !password) { return this.fail(new BadRequestError(options.badRequestMessage || 'Missing credentials')); } var self = this; var http_library = https; var parsedUrl = URL.parse(this._crowdServer, true); if (parsedUrl.protocol == "https:" && !parsedUrl.port) { parsedUrl.port = 443; } // As this is OAUth2, we *assume* https unless told explicitly otherwise. if (parsedUrl.protocol != "https:") { http_library = http; } var postData = JSON.stringify({ "value":password }); var applicationAuth = 'Basic ' + new Buffer(this._crowdApplication + ':' + this._crowdApplicationPassword).toString('base64'); function verified(err, user, info) { if (err) { return self.error(err); } if (!user) { return self.fail(info); } self.success(user, info); } function handleGroupResponse(response, result) { if (response.statusCode === 200) { var resultObject = JSON.parse(result); var groups = []; resultObject.groups.forEach(function (group) { // JIRA uses an older version of the Crowd REST API if (group.GroupEntity) { groups.push(group.GroupEntity.name); } else { groups.push(group.name); } }); return groups; } else if (response.statusCode >= 400 && response.statusCode < 500) { var error = JSON.parse(result); return self.fail(error); } else { return self.error(new Error("Invalid response from Crowd Server '" + self._crowdServer + "' [" + response.statusCode + "]: " + result)); } } function requestOptions(options) { ['ca'].forEach(function (property) { if (self._requestOptions[property]) { options[property] = self._requestOptions[property]; } }); if ('_ignoreSSL' in self) { options['rejectUnauthorized'] = !self._ignoreSSL; } if (self._proxyURL) { if ('http:' === self._crowdProtocol) { options['agent'] = new HttpProxyAgent(self._proxyURL); } else if ('https:' === self._crowdProtocol) { options['agent'] = new HttpsProxyAgent(self._proxyURL); } } return options; } function handleAuthenticationResponse(response, result) { if (response.statusCode === 200) { var crowdUser = JSON.parse(result); var userprofile = _parseProfile(crowdUser); userprofile._raw = result; if (self._retrieveGroupMemberships) { var groupResult = ""; var groupRequest = http_library.get(requestOptions({ host:parsedUrl.hostname, port:parsedUrl.port, path:parsedUrl.pathname + "rest/usermanagement/latest/user/group/nested?username=" + encodeURI(username), headers:{ "Content-Type":"application/json", "Accept":"application/json", "Authorization":applicationAuth } }), function (response) { _handleResponse(response, function (response, groupResult) { userprofile.groups = handleGroupResponse(response, groupResult); return self._verify(userprofile, verified); }); }); groupRequest.on('error', function (err) { self.error(new Error("Error connecting to Crowd Server '" + self._crowdServer + "': " + err)); }); } else { return self._verify(userprofile, verified); } } else if (response.statusCode >= 400 && response.statusCode < 500) { var error = {"message":result}; try { error = JSON.parse(result); } catch (err) { } return self.fail(error); } else { return self.error(new Error("Invalid response from Crowd Server '" + self._crowdServer + "' [" + response.statusCode + "]: " + result)); } } var crowdRequest = http_library.request(requestOptions({ host:parsedUrl.hostname, port:parsedUrl.port, path:parsedUrl.pathname + "rest/usermanagement/latest/authentication?expand=attributes&username=" + encodeURI(username), method:"POST", headers:{ "Content-Type":"application/json; charset=utf-8", "Accept":"application/json", "Content-Length": Buffer.byteLength(postData,'utf8'), "Authorization":applicationAuth } }), function (response) { _handleResponse(response, handleAuthenticationResponse); }); var timeoutMs = this._timeoutMs; crowdRequest.setTimeout(timeoutMs, function () { self.error(new Error("Error connecting to Crowd Server '" + self._crowdServer + "': " + "Timeout of " + timeoutMs + "ms reached")); }); crowdRequest.on('error', function (err) { self.error(new Error("Error connecting to Crowd Server '" + self._crowdServer + "': " + err)); }); crowdRequest.write(postData,'utf8'); crowdRequest.end(); }; /** * Expose `Strategy`. */ module.exports = Strategy;