diff --git a/lib/cas.js b/lib/cas.js index 3a49580..55f6f86 100644 --- a/lib/cas.js +++ b/lib/cas.js @@ -18,6 +18,8 @@ var http = require('http'); var https = require('https'); var url = require('url'); var cheerio = require('cheerio'); +var parseXML = require('xml2js').parseString; +var XMLprocessors = require('xml2js/lib/processors'); @@ -25,27 +27,27 @@ var cheerio = require('cheerio'); * Initialize CAS with the given `options`. * * @param {Object} options - * { - * 'base_url': + * { + * 'base_url': * The full URL to the CAS server, including the base path. - * 'service': + * 'service': * The URL of the page being authenticated. Can be omitted here and * specified during validate(). Or detected automatically during * authenticate(). - * 'version': + * 'version': * Either 1.0 or 2.0 * * * 'external_pgt_url': * (optional) The URL of the PGT callback server. * e.g. https://callback.example.com:8989/ - * The CAS server will try to contact this host every time a user - * logs in. + * The CAS server will try to contact this host every time a user + * logs in. * Do not use with the `pgt_server` option. * * * 'pgt_server': (previously 'proxy_server') - * (optional) Set to TRUE if you want to automatically start + * (optional) Set to TRUE if you want to automatically start * a PGT callback server internally. * Can be used to create a standalone PGT callback server. * Do not combine with `external_pgt_url`. @@ -62,13 +64,13 @@ var cheerio = require('cheerio'); * * * 'ssl_key': (previously 'proxy_server_key') - * A string value of your SSL private key. + * A string value of your SSL private key. * Required for `pgt_server`. * Optional for `external_pgt_url`. * Not needed otherwise. * * 'ssl_cert': (previously 'proxy_server_cert') - * A string value of your SSL certificate. + * A string value of your SSL certificate. * Required for `pgt_server`. * Optional for `external_pgt_url`. * Not needed otherwise. @@ -87,10 +89,10 @@ var cheerio = require('cheerio'); * } * @api public */ -var CAS = module.exports = function CAS(options) +var CAS = module.exports = function CAS(options) { options = options || {}; - + // Backwards compatibility for old option names options.pgt_server = options.pgt_server || options.proxy_server; options.pgt_host = options.pgt_host || options.proxy_callback_host; @@ -101,16 +103,16 @@ var CAS = module.exports = function CAS(options) if (!options.base_url) { throw new Error('Required CAS option `base_url` missing.'); - } + } var cas_url = url.parse(options.base_url); if (cas_url.protocol != 'https:') { throw new Error('Only https CAS servers are supported.'); - } + } if (!cas_url.hostname) { throw new Error('Option `base_url` must be a valid url like: https://example.com/cas'); - } - + } + this.version = options.version || 1.0; this.hostname = cas_url.hostname; this.port = cas_url.port || 443; @@ -118,17 +120,17 @@ var CAS = module.exports = function CAS(options) this.service = options.service; this.pgtStore = {}; this.pgt_is_external = false; - + // SSL options used when running a PGT callback server, // or as a client contacting the external PGT URL. this.ssl_cert = options.ssl_cert || null; this.ssl_key = options.ssl_key || null; this.ssl_ca = options.ssl_ca || null; - + // Setting this to false will allow cause bad SSL certificates to still // be accepted. Use only for testing. this.secureSSL = true; - + // Optional single sign out server list if (options.sso_servers) { this.ssoServers = options.sso_servers; @@ -145,7 +147,7 @@ var CAS = module.exports = function CAS(options) } this.is_pgt_external = true; this.pgt_url = url.format(pgt_url); - + // Deprecated if (options.external_proxy_url) { var proxy_url = url.parse(options.external_proxy_url); @@ -169,7 +171,7 @@ var CAS = module.exports = function CAS(options) //// Optional this.proxy_server_port = options.proxy_server_port || 0; // deprecated this.pgt_port = options.pgt_port || 80443 - + this.startPgtServer(this.ssl_key, this.ssl_cert, this.ssl_ca, this.pgt_host, this.pgt_port, this.proxy_server_port); } @@ -185,7 +187,7 @@ CAS.version = '0.0.5'; /** - * Force CAS authentication on a web page. If users are not yet authenticated, + * Force CAS authentication on a web page. If users are not yet authenticated, * they will be redirected to the CAS server to log in there. * * @param {object} req @@ -195,7 +197,7 @@ CAS.version = '0.0.5'; * @param {function} callback * callback(err, status, username, extended) * @param {String} service - * (optional) The URL of the service/page that requires authentication. + * (optional) The URL of the service/page that requires authentication. * Default is to extract this automatically from * the `req` object. * @api public @@ -204,7 +206,7 @@ CAS.prototype.authenticate = function(req, res, callback, service) { var casURL = 'https://' + this.hostname + ':' + this.port + this.base_path; var reqURL = url.parse(req.url, true); - + // Try to extract the CAS ticket from the URL var ticket = reqURL.query['ticket']; @@ -219,7 +221,7 @@ CAS.prototype.authenticate = function(req, res, callback, service) query: reqURL.query }); } - + // No ticket, so we haven't been sent to the CAS server yet if (!ticket) { // redirect to CAS server now @@ -229,7 +231,7 @@ CAS.prototype.authenticate = function(req, res, callback, service) res.end(); } - // We have a ticket! + // We have a ticket! else { // Validate it with the CAS server now this.validate(ticket, callback, service); @@ -239,7 +241,7 @@ CAS.prototype.authenticate = function(req, res, callback, service) /** - * Handle a single sign-out request from the CAS server. + * Handle a single sign-out request from the CAS server. * * In CAS 3.x the server keeps track of all the `ticket` and `service` values * associated with each user. Then when the user logs out from one site, the @@ -247,11 +249,11 @@ CAS.prototype.authenticate = function(req, res, callback, service) * a sign-out request containing the original `ticket` used to login. * * This is optional. But if you do use this, it must come before authenticate(). - * Also, it will only work if the service is accessible on the network by the + * Also, it will only work if the service is accessible on the network by the * CAS server. * - * Unlike the other functions in this module, this one will only work - * with Express or something else that pre-processes the body of a POST + * Unlike the other functions in this module, this one will only work + * with Express or something else that pre-processes the body of a POST * request. It is not compatible with basic node.js http req objects. * * @param {Object} req @@ -273,10 +275,10 @@ CAS.prototype.handleSingleSignout = function(req, res, next, logoutCallback) // not a recognized single signout server return next(); } - + try { // This was a signout request. Parse the XML. - var $ = cheerio.load(req.body['logoutRequest']); + var $ = cheerio.load(req.body['logoutRequest'], {xmlMode: true}); var ticketElems = $('samlp\\:SessionIndex'); if (ticketElems && ticketElems.length > 0) { // This is the ticket that was issued by CAS when the user @@ -310,8 +312,8 @@ CAS.prototype.handleSingleSignout = function(req, res, next, logoutCallback) * @param {String} returnUrl * (optional) The URL that the user will return to after logging out. * @param {Boolean} doRedirect - * (optional) Set this to TRUE to have the CAS server redirect the user - * automatically. Default is for the CAS server to only provide a + * (optional) Set this to TRUE to have the CAS server redirect the user + * automatically. Default is for the CAS server to only provide a * hyperlink to be clicked on. * @api public */ @@ -328,7 +330,7 @@ CAS.prototype.logout = function(req, res, returnUrl, doRedirect) // Logout with no way back logout_path = '/logout'; } - + var redirectURL = 'https://' + this.hostname + ':' + this.port + this.base_path + logout_path; res.writeHead(307, {'Location' : redirectURL}); res.write('CAS logout'); @@ -354,32 +356,43 @@ CAS.prototype.logout = function(req, res, returnUrl, doRedirect) * @param {String} service * The URL of the service requesting authentication. Optional if * the `service` option was already specified during initialization. - * @param {Boolean} renew + * @param {Boolean} renew * (optional) Set this to TRUE to force the CAS server to request * credentials from the user even if they had already done so * recently. * @api public */ -CAS.prototype.validate = function(ticket, callback, service, renew) +CAS.prototype.validate = function(ticket, callback, service, renew) { // Use different CAS path depending on version var validate_path; var pgtURL; var cas_version = this.version; - if (this.version < 2.0) { - // CAS 1.0 - validate_path = 'validate'; - } else { - // CAS 2.0 - pgtURL = this.pgt_url; - if (ticket.indexOf('PT-') == 0) { - validate_path = 'proxyValidate'; - } else { - //validate_path = 'serviceValidate'; - validate_path = 'proxyValidate'; - } + + var requestOptions = { + host: this.hostname, + port: this.port, + ca: this.ssl_ca || null, + rejectUnauthorized: this.secureSSL + }; + + var validateUri; + switch(cas_version){ + case '1.0': + validateUri = '/validate'; break; + case '2.0': + validateUri = '/serviceValidate'; break; + case '3.0': + validateUri = '/p3/serviceValidate'; break; + case 'saml1.1': + validateUri = '/samlValidate'; break; } - + + // CAS version check + if (!validateUri) { + throw new Error('Incorrect CAS Version.'); + } + // Service URL can be specified in the function call, or during // initialization. var service_url = service || this.service; @@ -387,29 +400,53 @@ CAS.prototype.validate = function(ticket, callback, service, renew) throw new Error('Required CAS option `service` missing.'); } - var query = { - 'ticket': ticket, - 'service': service_url - }; - if (renew) { - query['renew'] = 1; - } - if (pgtURL) { - query['pgtUrl'] = pgtURL; - } - - var queryPath = url.format({ - pathname: this.base_path+'/'+validate_path, + if (['1.0', '2.0', '3.0'].indexOf(cas_version) >= 0) { + var query = { + 'ticket': ticket, + 'service': service_url + }; + if (renew) { + query['renew'] = 1; + } + if (pgtURL) { + query['pgtUrl'] = pgtURL; + } + requestOptions.method = 'GET'; + requestOptions.path = url.format({ + pathname: this.base_path + validateUri, query: query }); + } else if (cas_version === 'saml1.1') { + var now = new Date(); + var post_data = '\n' + + '\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' ' + ticket + '\n' + + ' \n' + + ' \n' + + ' \n' + + ''; - var req = https.get({ - host: this.hostname, - port: this.port, - path: queryPath, - ca: this.ssl_ca || null, - rejectUnauthorized: this.secureSSL - }, function(res) { + requestOptions.method = 'POST'; + requestOptions.path = url.format({ + pathname: this.base_path + validateUri, + query: { + TARGET: service_url, + ticket: '' + } + }); + requestOptions.headers = { + 'Content-Type': 'text/xml', + 'Content-Length': Buffer.byteLength(post_data) + }; + } + + var req = https.request(requestOptions, function(res) { // Handle server errors res.on('error', function(e) { callback(e); @@ -426,8 +463,9 @@ CAS.prototype.validate = function(ticket, callback, service, renew) }); res.on('end', function() { + // console.log('cas validate response', response); // CAS 1.0 - if (cas_version < 2.0) { + if (cas_version == '1.0') { var sections = response.split('\n'); if (sections.length >= 1) { if (sections[0] == 'no') { @@ -440,13 +478,59 @@ CAS.prototype.validate = function(ticket, callback, service, renew) } // Format was not correct, error callback(new Error('Bad response format.')); - } - + } + + else if (cas_version == 'saml1.1'){ + parseXML(response, { + trim: true, + normalize: true, + explicitArray: false, + tagNameProcessors: [XMLprocessors.normalize, XMLprocessors.stripPrefix] + }, function(err, result) { + if (err) { + return callback(new Error('Response from CAS server was bad.')); + } + try { + var samlResponse = result.envelope.body.response; + var success = samlResponse.status.statuscode.$.Value.split(':')[1]; + if (success !== 'Success') { + return callback(new Error('CAS authentication failed (' + success + ').')); + } else { + var attributes = {}; + var attributesArray = samlResponse.assertion.attributestatement.attribute; + if (!(attributesArray instanceof Array)) { + attributesArray = [attributesArray]; + } + attributesArray.forEach(function(attr) { + var thisAttrValue; + if (attr.attributevalue instanceof Array) { + thisAttrValue = []; + attr.attributevalue.forEach(function(v) { + thisAttrValue.push(v._); + }); + } else { + thisAttrValue = attr.attributevalue._; + } + attributes[attr.$.AttributeName] = thisAttrValue; + }); + return callback(null, true, samlResponse.assertion.authenticationstatement.subject.nameidentifier, { + attributes: attributes, + username: samlResponse.assertion.authenticationstatement.subject.nameidentifier, + ticket: ticket + }); + } + } catch (err) { + console.log(err); + return callback(new Error('CAS authentication failed.'), false); + } + }); + } + // CAS 2.0 (XML response, and extended attributes) else { // Use cheerio to parse the XML repsonse. - var $ = cheerio.load(response); - + var $ = cheerio.load(response, {xmlMode: true}); + // Check for auth success var elemSuccess = $('cas\\:authenticationSuccess').first(); if (elemSuccess && elemSuccess.length > 0) { @@ -459,14 +543,14 @@ CAS.prototype.validate = function(ticket, callback, service, renew) // Got username var username = elemUser.text(); - + // Look for optional proxy granting ticket var pgtIOU; var elemPGT = elemSuccess.find('cas\\:proxyGrantingTicket').first(); if (elemPGT) { pgtIOU = elemPGT.text(); } - + // Look for optional proxies var proxies = []; var elemProxies = elemSuccess.find('cas\\:proxies'); @@ -477,7 +561,7 @@ CAS.prototype.validate = function(ticket, callback, service, renew) // Look for optional attributes var attributes = parseAttributes(elemSuccess); - + callback(undefined, true, username, { 'username': username, 'attributes': attributes, @@ -505,12 +589,17 @@ CAS.prototype.validate = function(ticket, callback, service, renew) }; }); }); - + // Connection error with the CAS server req.on('error', function(err) { callback(err); req.abort(); }); + + if (cas_version === 'saml1.1') { + req.write(post_data); + } + req.end(); }; @@ -530,7 +619,7 @@ CAS.prototype.validate = function(ticket, callback, service, renew) CAS.prototype.getProxyGrantingTicket = function(pgtIOU, callback) { var pgt = ''; - + // If configured for external PGT server use, fetch the PT from there if (this.is_pgt_external) { var urlFetchPGT = url.parse(this.pgt_url + 'getPGT?pgtiou=' + pgtIOU); @@ -538,7 +627,7 @@ CAS.prototype.getProxyGrantingTicket = function(pgtIOU, callback) urlFetchPGT.cert = this.ssl_cert || null; urlFetchPGT.ca = this.ssl_ca || null; urlFetchPGT.rejectUnauthorized = this.secureSSL; - + var req = https.get(urlFetchPGT, function(res) { res.on('data', function(chunk) { pgt += chunk; @@ -553,7 +642,7 @@ CAS.prototype.getProxyGrantingTicket = function(pgtIOU, callback) callback(err); }); }); - + // Error starting the connection to the PGT server req.on('error', function(err) { callback(err); @@ -561,7 +650,7 @@ CAS.prototype.getProxyGrantingTicket = function(pgtIOU, callback) }); return null; } - + // Look up the PGT locally else if (this.pgtStore[pgtIOU]) { pgt = this.pgtStore[pgtIOU]['pgtID']; @@ -596,10 +685,10 @@ CAS.prototype.getProxyGrantingTicket = function(pgtIOU, callback) * callback(err, pt) * @api public */ -CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) +CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) { var self = this; - + // Obtain the PGT this.getProxyGrantingTicket(pgtIOU, function(err, pgt) { if (err) { @@ -615,7 +704,7 @@ CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) rejectUnauthorized: self.secureSSL, path: url.format({ pathname: self.base_path + '/proxy', - query: { + query: { 'targetService': targetService, 'pgt': pgt } @@ -625,20 +714,20 @@ CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) res.on('error', function(e) { callback(e); }); - + // Read result res.setEncoding('utf8'); var response = ''; res.on('data', function(chunk) { response += chunk; if (response.length > 1e6) { - req.connection.destroy(); + req.connection.destroy(); } }); res.on('end', function() { // Use cheerio to parse the XML response - var $ = cheerio.load(response); - + var $ = cheerio.load(response, {xmlMode: true}); + // Got the proxy ticket var elemTicket = $('cas\\:proxyTicket').first(); if (elemTicket && elemTicket.length > 0) { @@ -672,8 +761,8 @@ CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) /** * Start a PGT callback server. - * - * This is a local HTTPS server that listens for incoming connections from + * + * This is a local HTTPS server that listens for incoming connections from * the CAS server. Any PGTs received from the CAS server will be stored * in `this.pgtStore`. * @@ -683,9 +772,9 @@ CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) * * The following functionality is deprecated: * - * This is optionally also an http proxy server that listens for outgoing - * requests from clients that already have a PGTIOU. In addition to the - * normal HTTP information, the client must also supply these two headers + * This is optionally also an http proxy server that listens for outgoing + * requests from clients that already have a PGTIOU. In addition to the + * normal HTTP information, the client must also supply these two headers * in the request: * cas-proxy-pgtiou * cas-proxy-targeturl @@ -708,7 +797,7 @@ CAS.prototype.getProxyTicket = function(pgtIOU, targetService, callback) * internal requests via CAS.proxiedRequest(). * @api public */ -CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPort, proxyPort) +CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPort, proxyPort) { var serverOptions = { 'key': key, @@ -716,17 +805,17 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor 'ca': ca }; var self = this; - + // This is the pgtURL that will be sent to the CAS server during a // validation request. The CAS server will try to connect to it. this.pgt_url = 'https://' + callbackHost + ':' + callbackPort + '/'; - + // PGT callback server that listens for incoming connections from // the CAS server. var pgtServer = https.createServer(serverOptions); console.log('Starting PGT callback server on port ' + callbackPort); pgtServer.addListener("request", function(req, res) { - + var reqURL = url.parse(req.url, true); // Check if this is a request from a CAS _client_ to get a PGT. @@ -745,7 +834,7 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor } return; } - + // Otherwise this is a connection from the CAS _server_. // The incoming connection tells us what the PGTIOU and PGT values // are. It expects only a HTTP 200 response in return. @@ -765,7 +854,7 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor } }); pgtServer.listen(callbackPort); - + // Start an interval for garbage collection of the local PGT store. if (this.pgtInterval) { clearInterval(this.pgtInterval); @@ -780,8 +869,8 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor } } }, 1000 * 60); - - + + // Deprecated: // Proxy server that listens for HTTPS connections from other CAS clients // and forwards them to the target service. Disabled by default. @@ -789,7 +878,7 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor var proxyServer = https.createServer(serverOptions); console.log('Starting CAS-aware HTTP proxy server on port ' + proxyPort); proxyServer.addListener("request", function(req, res) { - // Use "cas-proxy-..." headers to obtain information about the + // Use "cas-proxy-..." headers to obtain information about the // requested target service. try { var pgtIOU = req.headers['cas-proxy-pgtiou']; @@ -818,7 +907,7 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor res.end(); return; } - + // The headers are okay. Next begin the proxied request. targetOptions.method = req.method || 'GET'; self.proxiedRequest(pgtIOU, targetOptions, function(err, targetReq, targetRes) { @@ -829,7 +918,7 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor res.end(); return; } - + // Mirror requester's data to the target req.on('data', function(chunk) { targetReq.write(chunk); @@ -837,10 +926,10 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor req.on('end', function() { targetReq.end(); }); - + // Mirror target's response headers back to requester res.writeHead(targetRes.statusCode, targetRes.headers); - + // Mirror target's data back to the requester targetRes.on('data', function(chunk) { res.write(chunk); @@ -857,7 +946,7 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor /** * Create a CAS proxied HTTP/HTTPS request. - * The CAS proxy ticket (PT) will automatically be added to the target + * The CAS proxy ticket (PT) will automatically be added to the target * service's query. * * Deprecated. @@ -866,13 +955,13 @@ CAS.prototype.startPgtServer = function(key, cert, ca, callbackHost, callbackPor * This should have been obtained during the initial CAS login with * the validate() function. * @param {Object} options - * Same as the options passed in to http.request(). This is where you + * Same as the options passed in to http.request(). This is where you * specify the service URL you are requesting. * @param {Function} callback * callback(err, req, res) * @api public */ -CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) +CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) { if (this.external_proxy_url) { this.proxiedRequestExternal(this.external_proxy_url, pgtIOU, options, callback); @@ -880,13 +969,13 @@ CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) } var targetService = url.format(options); - + this.getProxyTicket(pgtIOU, targetService, function(err, pt) { if (err) { callback(err); return; } - + // Add the proxy ticket to the target service's query string var path = options.path || targetService.replace(/^https?:\/\/[^\/]+/, ''); if (path.match(/[&?]/)) { @@ -899,7 +988,7 @@ CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) delete options.href; delete options.query; options.agent = false; - + // Request the target service var serviceObj; if (options.options == 'https:') { @@ -917,7 +1006,7 @@ CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) callback(undefined, req, res); }); break; - + case 'POST': case 'PUT': // Let the calling function end the request manually after @@ -933,7 +1022,7 @@ CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) catch (err) { callback(err); } - + }); } @@ -949,13 +1038,13 @@ CAS.prototype.proxiedRequest = function(pgtIOU, options, callback) * This should have been obtained during the initial CAS login with * the validate() function. * @param {Object} requestOptions - * Same as the options passed in to http.request(). This is where you + * Same as the options passed in to http.request(). This is where you * specify the service URL you are requesting. * @param {Function} callback * callback(err, req, res) * @api public */ -CAS.prototype.proxiedRequestExternal = function(proxyURL, pgtIOU, options, callback) +CAS.prototype.proxiedRequestExternal = function(proxyURL, pgtIOU, options, callback) { var targetService = url.format(options); var proxyInfo = url.parse(proxyURL); @@ -975,7 +1064,7 @@ CAS.prototype.proxiedRequestExternal = function(proxyURL, pgtIOU, options, callb } else { serviceObj = http; } - + try { var req = serviceObj.get(proxyInfo, function(res) { callback(undefined, req, res); @@ -1000,13 +1089,13 @@ CAS.prototype.proxiedRequestExternal = function(proxyURL, pgtIOU, options, callb * } * @attribution http://downloads.jasig.org/cas-clients/php/1.2.0/docs/api/client_8php_source.html#l01589 */ -var parseAttributes = function(elemSuccess) +var parseAttributes = function(elemSuccess) { var attributes = {}; var elemAttribute = elemSuccess.find('cas\\:attributes').first(); if (elemAttribute && elemAttribute.children().length > 0) { // "Jasig Style" Attributes: - // + // // // // jsmith @@ -1023,7 +1112,7 @@ var parseAttributes = function(elemSuccess) // for (var i=0; i // // jsmith - // + // // RubyCAS // Smith // John // CN=Staff,OU=Groups,DC=example,DC=edu // CN=Spanish Department,OU=Departments,... - // + // // PGTIOU-84678-8a9d2... // // - // + // for (var i=0; i // // jsmith - // + // // // // // // - // + // // PGTIOU-84678-8a9d2sfa23casd // // @@ -1111,6 +1200,6 @@ var parseAttributes = function(elemSuccess) } } } - + return attributes; } diff --git a/package.json b/package.json index e93f1e9..77346e7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cas", - "version": "0.0.5", + "version": "0.0.7", "description": "Central Authentication Service (CAS) client for Node.js", "keywords": [ "cas", @@ -18,7 +18,8 @@ } ], "dependencies": { - "cheerio": "0.19.0" + "cheerio": "0.22.0", + "xml2js": "^0.4.8" }, "devDependencies": { "chai": "^3.4.1",