pax_global_header00006660000000000000000000000064123265527140014521gustar00rootroot0000000000000052 comment=d9ccd0f840a085462d1409f4d53b1260501e42fe node-openid-0.5.9/000077500000000000000000000000001232655271400137355ustar00rootroot00000000000000node-openid-0.5.9/.gitattributes000066400000000000000000000001531232655271400166270ustar00rootroot00000000000000# Normalize line endings to LF for text files * text=auto # Explicitly declare JS files as text *.js text node-openid-0.5.9/.gitignore000066400000000000000000000000161232655271400157220ustar00rootroot00000000000000node_modules/ node-openid-0.5.9/LICENSE000066400000000000000000000020501232655271400147370ustar00rootroot00000000000000Copyright (C) 2010 by Håvard Stranden. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. node-openid-0.5.9/README.md000066400000000000000000000151621232655271400152210ustar00rootroot00000000000000# OpenID for Node.js OpenID for Node.js is (yes, you guessed it) an OpenID implementation for Node.js. Highlights and features include: - Full OpenID 1.0/1.1/2.0 compliant Relying Party (client) implementation - Very simple API - Simple extension points for association state ## Download The library can be [reviewed and retrieved from GitHub](http://github.com/havard/node-openid). ## Installation If you use [`npm`](http://npmjs.org), simply do `npm install openid`. Otherwise, you can grab the code from [GitHub](https://github.com/havard/node-openid). ## Examples Here's a very simple server using OpenID for Node.js for authentication: ```javascript var openid = require('openid'); var url = require('url'); var querystring = require('querystring'); var relyingParty = new openid.RelyingParty( 'http://example.com/verify', // Verification URL (yours) null, // Realm (optional, specifies realm for OpenID authentication) false, // Use stateless verification false, // Strict mode []); // List of extensions to enable and include var server = require('http').createServer( function(req, res) { var parsedUrl = url.parse(req.url); if(parsedUrl.pathname == '/authenticate') { // User supplied identifier var query = querystring.parse(parsedUrl.query); var identifier = query.openid_identifier; // Resolve identifier, associate, and build authentication URL relyingParty.authenticate(identifier, false, function(error, authUrl) { if (error) { res.writeHead(200); res.end('Authentication failed: ' + error.message); } else if (!authUrl) { res.writeHead(200); res.end('Authentication failed'); } else { res.writeHead(302, { Location: authUrl }); res.end(); } }); } else if(parsedUrl.pathname == '/verify') { // Verify identity assertion // NOTE: Passing just the URL is also possible relyingParty.verifyAssertion(req, function(error, result) { res.writeHead(200); res.end(!error && result.authenticated ? 'Success :)' : 'Failure :('); }); } else { // Deliver an OpenID form on all other URLs res.writeHead(200); res.end('' + '
' + '

Login using OpenID

' + '' + '' + '
'); } }); server.listen(80); ``` A more elaborate example including extensions can be found in `sample.js` in the GitHub repository. ## Supported Extensions This library comes with built-in support for the following OpenID extensions: - The Simple Registration (SREG) 1.1 extension is implemented as `openid.SimpleRegistration`. - The Attribute Exchange (AX) 1.0 extension is implemented as `openid.AttributeExchange`. - The OAuth 1.0 extension is implemented as `openid.OAuthHybrid`. - The User Interface 1.0 extension is implemented as `openid.UserInterface`. - The Provider Authentication Policy Extension 1.0 (PAPE) is implemented as `openid.pape`. ## Storing association state To provide a way to save/load association state, you need to mix-in two functions in the `openid` module: - `saveAssociation(provider, type, handle, secret, expiry_time_in_seconds, callback)` is called when a new association is established during authentication. The callback should be called with any error as its first argument (or `null` if no error occured). - `loadAssociation(handle, callback)` is used to retrieve the association identified by `handle` when verification happens. The callback should be called with any error as its first argument (and `null` as the second argument), or an object with the keys `provider`, `type`, `secret` if the association was loaded successfully. The `openid` module includes default implementations for these functions using a simple object to store the associations in-memory. ## Caching discovered information The verification of a positive assertion (i.e. an authenticated user) can be sped up significantly by avoiding the need for additional provider discoveries when possible. In order to achieve, this speed-up, node-openid needs to cache its discovered providers. You can mix-in two functions to override the default cache, which is an in-memory cache utilizing a simple object store: - `saveDiscoveredInformation(key, provider, callback)` is used when saving a discovered provider. The following behavior is required: - The `key` parameter should be uses as a key for storing the provider - it will be used as the lookup key when loading the provider. (Currently, the key is either a claimed identifier or an OP-local identifier, depending on the OpenID context.) - When saving fails for some reason, `callback(error)` is called with `error` being an error object specifying what failed. - When saving succeeds, `callback(null)` is called. - `loadDiscoveredInformation(key, callback)` is used to load any previously discovered information about the provider for an identifier. The following behavior is required: - When no provider is found for the identifier, `callback(null, null)` is called (i.e. it is not an error to not have any data to return). - When loading fails for some reason, `callback(error, null)` is called with `error` being an error string specifying why loading failed. - When loading succeeds, `callback(null, provider)` is called with the exact provider object that was previously stored using `saveDiscoveredInformation`. ## Proxy Support `node-openid` makes HTTP and HTTPS requests during authentication. You can have these requests go through a proxy server, by using the following environment variables: - HTTP_PROXY_HOST and HTTP_PROXY_PORT control how http:// requests are sent - HTTPS_PROXY_HOST and HTTPS_PROXY_PORT control how https:// requests are sent ## License OpenID for Node.js is licensed under the MIT license. See LICENSE for further details. The libary includes bigint functionality released by Tom Wu under the BSD license, and Base64 functions released by Nick Galbreath under the MIT license. Please see `lib/bigint.js` and `lib/base64.js` for the details of the licenses for these functions. node-openid-0.5.9/expressjs_sample/000077500000000000000000000000001232655271400173245ustar00rootroot00000000000000node-openid-0.5.9/expressjs_sample/authentication_controller.js000066400000000000000000000025251232655271400251500ustar00rootroot00000000000000var openid = require('openid'); var relyingParty = new openid.RelyingParty( 'http://localhost:8888/login/verify', // Verification URL (yours) null, // Realm (optional, specifies realm for OpenID authentication) false, // Use stateless verification false, // Strict mode []); // List of extensions to enable and include app.get('/login', function(request, response) { response.render('login'); }); app.get('/login/authenticate', function(request, response) { var identifier = request.query.openid_identifier; // Resolve identifier, associate, and build authentication URL relyingParty.authenticate(identifier, false, function(error, authUrl) { if (error) { response.writeHead(200); response.end('Authentication failed: ' + error.message); } else if (!authUrl) { response.writeHead(200); response.end('Authentication failed'); } else { response.writeHead(302, { Location: authUrl }); response.end(); } }); }); app.get('/login/verify', function(request, response) { // Verify identity assertion // NOTE: Passing just the URL is also possible relyingParty.verifyAssertion(request, function(error, result) { response.writeHead(200); response.end(!error && result.authenticated ? 'Success :)' // TODO: redirect to something interesting! : 'Failure :('); // TODO: show some error message! }); });node-openid-0.5.9/expressjs_sample/login.jade000066400000000000000000000004021232655271400212550ustar00rootroot00000000000000doctype 5 html head title OpenId Authentication with expressjs sample body div#main h1 Log in using your OpenId account form(method='get', action='/login/authenticate') input(name='openid_identifier') input(type='submit', value='Login') node-openid-0.5.9/lib/000077500000000000000000000000001232655271400145035ustar00rootroot00000000000000node-openid-0.5.9/lib/base64.js000066400000000000000000000120701232655271400161250ustar00rootroot00000000000000/* Base64 conversion functions * * Adaptions for node.js are Copyright (c) 2010 Håvard Stranden * * Copyright (c) 2010 Nick Galbreath * http://code.google.com/p/stringencoders/source/browse/#svn/trunk/javascript * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation * files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, * copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * OTHER DEALINGS IN THE SOFTWARE. * * base64 encode/decode compatible with window.btoa/atob * * window.atob/btoa is a Firefox extension to convert binary data (the "b") * to base64 (ascii, the "a"). * * It is also found in Safari and Chrome. It is not available in IE. * * if (!window.btoa) window.btoa = base64.encode * if (!window.atob) window.atob = base64.decode * * The original spec's for atob/btoa are a bit lacking * https://developer.mozilla.org/en/DOM/window.atob * https://developer.mozilla.org/en/DOM/window.btoa * * window.btoa and base64.encode takes a string where charCodeAt is [0,255] * If any character is not [0,255], then an exception is thrown. * * window.atob and base64.decode take a base64-encoded string * If the input length is not a multiple of 4, or contains invalid characters * then an exception is thrown. * * -*- Mode: JS; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set sw=2 ts=2 et tw=80 : */ var base64 = {}; base64.PADCHAR = '='; base64.ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; base64.getbyte64 = function(s,i) { // This is oddly fast, except on Chrome/V8. // Minimal or no improvement in performance by using a // object with properties mapping chars to value (eg. 'A': 0) var idx = base64.ALPHA.indexOf(s.charAt(i)); if (idx == -1) { throw "Cannot decode base64"; } return idx; } base64.decode = function(s) { // convert to string s = "" + s; var getbyte64 = base64.getbyte64; var pads, i, b10; var imax = s.length if (imax == 0) { return s; } if (imax % 4 != 0) { throw "Cannot decode base64"; } pads = 0 if (s.charAt(imax -1) == base64.PADCHAR) { pads = 1; if (s.charAt(imax -2) == base64.PADCHAR) { pads = 2; } // either way, we want to ignore this last block imax -= 4; } var x = []; for (i = 0; i < imax; i += 4) { b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6) | getbyte64(s,i+3); x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff, b10 & 0xff)); } switch (pads) { case 1: b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12) | (getbyte64(s,i+2) << 6) x.push(String.fromCharCode(b10 >> 16, (b10 >> 8) & 0xff)); break; case 2: b10 = (getbyte64(s,i) << 18) | (getbyte64(s,i+1) << 12); x.push(String.fromCharCode(b10 >> 16)); break; } return x.join(''); } base64.getbyte = function(s,i) { var x = s.charCodeAt(i); if (x > 255) { throw "INVALID_CHARACTER_ERR: DOM Exception 5"; } return x; } base64.encode = function(s) { if (arguments.length != 1) { throw "SyntaxError: Not enough arguments"; } var padchar = base64.PADCHAR; var alpha = base64.ALPHA; var getbyte = base64.getbyte; var i, b10; var x = []; // convert to string s = "" + s; var imax = s.length - s.length % 3; if (s.length == 0) { return s; } for (i = 0; i < imax; i += 3) { b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8) | getbyte(s,i+2); x.push(alpha.charAt(b10 >> 18)); x.push(alpha.charAt((b10 >> 12) & 0x3F)); x.push(alpha.charAt((b10 >> 6) & 0x3f)); x.push(alpha.charAt(b10 & 0x3f)); } switch (s.length - imax) { case 1: b10 = getbyte(s,i) << 16; x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + padchar + padchar); break; case 2: b10 = (getbyte(s,i) << 16) | (getbyte(s,i+1) << 8); x.push(alpha.charAt(b10 >> 18) + alpha.charAt((b10 >> 12) & 0x3F) + alpha.charAt((b10 >> 6) & 0x3f) + padchar); break; } return x.join(''); } exports.base64 = base64; node-openid-0.5.9/lib/convert.js000066400000000000000000000032321232655271400165210ustar00rootroot00000000000000/* Conversion functions used in OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * * -*- Mode: JS; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set sw=2 ts=2 et tw=80 : */ var base64 = require('./base64').base64; function btwoc(i) { if(i.charCodeAt(0) > 127) { return String.fromCharCode(0) + i; } return i; } function unbtwoc(i) { if(i[0] === String.fromCharCode(0)) { return i.substr(1); } return i; } exports.btwoc = btwoc; exports.unbtwoc = unbtwoc; exports.base64 = base64; node-openid-0.5.9/lib/xrds.js000066400000000000000000000056471232655271400160350ustar00rootroot00000000000000/* A simple XRDS and Yadis parser written for OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * * -*- Mode: JS; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set sw=2 ts=2 et tw=80 : */ exports.parse = function(data) { data = data.replace(/\r|\n/g, ''); var services = []; var serviceMatches = data.match(/(.*?)<\/Service>/g); if(!serviceMatches) { return services; } for(var s = 0, len = serviceMatches.length; s < len; ++s) { var service = serviceMatches[s]; var svcs = []; var priorityMatch = //g.exec(service); var priority = 0; if(priorityMatch) { priority = parseInt(priorityMatch[1], 10); } var typeMatch = null; var typeRegex = new RegExp('(.*?)<\\/Type\\s*?>', 'g'); while(typeMatch = typeRegex.exec(service)) { svcs.push({ priority: priority, type: typeMatch[2] }); } if(svcs.length == 0) { continue; } var idMatch = /<(Local|Canonical)ID\s*?>(.*?)<\/\1ID\s*?>/g.exec(service); if(idMatch) { for(var i = 0; i < svcs.length; i++) { var svc = svcs[i]; svc.id = idMatch[2]; } } var uriMatch = /(.*?)<\/URI\s*?>/g.exec(service); if(!uriMatch) { continue; } for(var i = 0; i < svcs.length; i++) { var svc = svcs[i]; svc.uri = uriMatch[2]; } var delegateMatch = /<(.*?Delegate)\s*?>(.*)<\/\1\s*?>/g.exec(service); if(delegateMatch) { svc.delegate = delegateMatch[2]; } services.push.apply(services, svcs); } services.sort(function(a, b) { return a.priority < b.priority ? -1 : (a.priority == b.priority ? 0 : 1); }); return services; } node-openid-0.5.9/openid.js000066400000000000000000001253021232655271400155540ustar00rootroot00000000000000/* OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * * -*- Mode: JS; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- * vim: set sw=2 ts=2 et tw=80 : */ var convert = require('./lib/convert'), crypto = require('crypto'), http = require('http'), https = require('https'), querystring = require('querystring'), url = require('url'), xrds = require('./lib/xrds'); var _associations = {}; var _discoveries = {}; var openid = exports; openid.RelyingParty = function(returnUrl, realm, stateless, strict, extensions) { this.returnUrl = returnUrl; this.realm = realm || null; this.stateless = stateless; this.strict = strict; this.extensions = extensions; } openid.RelyingParty.prototype.authenticate = function(identifier, immediate, callback) { openid.authenticate(identifier, this.returnUrl, this.realm, immediate, this.stateless, callback, this.extensions, this.strict); } openid.RelyingParty.prototype.verifyAssertion = function(requestOrUrl, callback) { openid.verifyAssertion(requestOrUrl, callback, this.stateless, this.extensions, this.strict); } var _isDef = function(e) { var undefined; return e !== undefined; } var _toBase64 = function(binary) { return convert.base64.encode(convert.btwoc(binary)); } var _fromBase64 = function(str) { return convert.unbtwoc(convert.base64.decode(str)); } var _xor = function(a, b) { if(a.length != b.length) { throw new Error('Length must match for xor'); } var r = ''; for(var i = 0; i < a.length; ++i) { r += String.fromCharCode(a.charCodeAt(i) ^ b.charCodeAt(i)); } return r; } openid.saveAssociation = function(provider, type, handle, secret, expiry_time_in_seconds, callback) { setTimeout(function() { openid.removeAssociation(handle); }, expiry_time_in_seconds * 1000); _associations[handle] = {provider: provider, type : type, secret: secret}; callback(null); // Custom implementations may report error as first argument } openid.loadAssociation = function(handle, callback) { if(_isDef(_associations[handle])) { callback(null, _associations[handle]); } else { callback(null, null); } } openid.removeAssociation = function(handle) { delete _associations[handle]; return true; } openid.saveDiscoveredInformation = function(key, provider, callback) { _discoveries[key] = provider; return callback(null); } openid.loadDiscoveredInformation = function(key, callback) { if(!_isDef(_discoveries[key])) { return callback(null, null); } return callback(null, _discoveries[key]); } var _buildUrl = function(theUrl, params) { theUrl = url.parse(theUrl, true); delete theUrl['search']; if(params) { if(!theUrl.query) { theUrl.query = params; } else { for(var key in params) { if(params.hasOwnProperty(key)) { theUrl.query[key] = params[key]; } } } } return url.format(theUrl); } var _proxyRequest = function(protocol, options) { /* If process.env['HTTP_PROXY_HOST'] and the env variable `HTTP_PROXY_POST` are set, make sure path and the header Host are set to target url. Similarly, `HTTPS_PROXY_HOST` and `HTTPS_PROXY_PORT` can be used to proxy HTTPS traffic. Proxies Example: export HTTP_PROXY_HOST=localhost export HTTP_PROXY_PORT=8080 export HTTPS_PROXY_HOST=localhost export HTTPS_PROXY_PORT=8442 Function returns protocol which should be used for network request, one of http: or https: */ var targetHost = options.host; var newProtocol = protocol; if (!targetHost) return; var updateOptions = function (envPrefix) { var proxyHostname = process.env[envPrefix + '_PROXY_HOST'].trim(); var proxyPort = parseInt(process.env[envPrefix + '_PROXY_PORT'], 10); if (proxyHostname.length > 0 && ! isNaN(proxyPort)) { if (! options.headers) options.headers = {}; var targetHostAndPort = targetHost + ':' + options.port; options.host = proxyHostname; options.port = proxyPort; options.path = protocol + '//' + targetHostAndPort + options.path; options.headers['Host'] = targetHostAndPort; } }; if ('https:' === protocol && !! process.env['HTTPS_PROXY_HOST'] && !! process.env['HTTPS_PROXY_PORT']) { updateOptions('HTTPS'); // Proxy server request must be done via http... it is responsible for // Making the https request... newProtocol = 'http:'; } else if (!! process.env['HTTP_PROXY_HOST'] && !! process.env['HTTP_PROXY_PORT']) { updateOptions('HTTP'); } return newProtocol; } var _get = function(getUrl, params, callback, redirects) { redirects = redirects || 5; getUrl = url.parse(_buildUrl(getUrl, params)); var path = getUrl.pathname || '/'; if(getUrl.query) { path += '?' + getUrl.query; } var options = { host: getUrl.hostname, port: _isDef(getUrl.port) ? parseInt(getUrl.port, 10) : (getUrl.protocol == 'https:' ? 443 : 80), headers: { 'Accept' : 'application/xrds+xml,text/html,text/plain,*/*' }, path: path }; var protocol = _proxyRequest(getUrl.protocol, options); (protocol == 'https:' ? https : http).get(options, function(res) { var data = ''; res.on('data', function(chunk) { data += chunk; }); var isDone = false; var done = function() { if (isDone) return; isDone = true; if(res.headers.location && --redirects) { var redirectUrl = res.headers.location; if(redirectUrl.indexOf('http') !== 0) { redirectUrl = getUrl.protocol + '//' + getUrl.hostname + ':' + options.port + (redirectUrl.indexOf('/') === 0 ? redirectUrl : '/' + redirectUrl); } _get(redirectUrl, params, callback, redirects); } else { callback(data, res.headers, res.statusCode); } } res.on('end', function() { done(); }); res.on('close', function() { done(); }); }).on('error', function(error) { return callback(error); }); } var _post = function(postUrl, data, callback, redirects) { redirects = redirects || 5; postUrl = url.parse(postUrl); var path = postUrl.pathname || '/'; if(postUrl.query) { path += '?' + postUrl.query; } var encodedData = _encodePostData(data); var options = { host: postUrl.hostname, path: path, port: _isDef(postUrl.port) ? postUrl.port : (postUrl.protocol == 'https:' ? 443 : 80), headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': encodedData.length }, method: 'POST' }; var protocol = _proxyRequest(postUrl.protocol, options); (protocol == 'https:' ? https : http).request(options, function(res) { var data = ''; res.on('data', function(chunk) { data += chunk; }); var isDone = false; var done = function() { if (isDone) return; isDone = true; if(res.headers.location && --redirects) { _post(res.headers.location, data, callback, redirects); } else { callback(data, res.headers, res.statusCode); } } res.on('end', function() { done(); }); res.on('close', function() { done(); }); }).on('error', function(error) { return callback(error); }).end(encodedData); } var _encodePostData = function(data) { var encoded = querystring.stringify(data); return encoded; } var _decodePostData = function(data) { var lines = data.split('\n'); var result = {}; for (var i = 0; i < lines.length ; i++) { var line = lines[i]; if (line.length > 0 && line[line.length - 1] == '\r') { line = line.substring(0, line.length - 1); } var colon = line.indexOf(':'); if (colon === -1) { continue; } var key = line.substr(0, line.indexOf(':')); var value = line.substr(line.indexOf(':') + 1); result[key] = value; } return result; } var _normalizeIdentifier = function(identifier) { identifier = identifier.replace(/^\s+|\s+$/g, ''); if(!identifier) return null; if(identifier.indexOf('xri://') === 0) { identifier = identifier.substring(6); } if(/^[(=@\+\$!]/.test(identifier)) { return identifier; } if(identifier.indexOf('http') === 0) { return identifier; } return 'http://' + identifier; } var _parseXrds = function(xrdsUrl, xrdsData) { var services = xrds.parse(xrdsData); if(services == null) { return null; } var providers = []; for(var i = 0, len = services.length; i < len; ++i) { var service = services[i]; var provider = {}; provider.endpoint = service.uri; if(/https?:\/\/xri./.test(xrdsUrl)) { provider.claimedIdentifier = service.id; } if(service.type == 'http://specs.openid.net/auth/2.0/signon') { provider.version = 'http://specs.openid.net/auth/2.0'; provider.localIdentifier = service.id; } else if(service.type == 'http://specs.openid.net/auth/2.0/server') { provider.version = 'http://specs.openid.net/auth/2.0'; } else if(service.type == 'http://openid.net/signon/1.0' || service.type == 'http://openid.net/signon/1.1') { provider.version = service.type; provider.localIdentifier = service.delegate; } else { continue; } providers.push(provider); } return providers; } var _matchMetaTag = function(html) { var metaTagMatches = //ig.exec(html); if(!metaTagMatches || metaTagMatches.length < 2) { return null; } var contentMatches = /content="(.*?)"/ig.exec(metaTagMatches[1]); if(!contentMatches || contentMatches.length < 2) { return null; } return contentMatches[1]; } var _matchLinkTag = function(html, rel) { var providerLinkMatches = new RegExp('', 'ig').exec(html); if(!providerLinkMatches || providerLinkMatches.length < 1) { return null; } var href = /href=["'](.*?)["']/ig.exec(providerLinkMatches[0]); if(!href || href.length < 2) { return null; } return href[1]; } var _parseHtml = function(htmlUrl, html, callback, hops) { var metaUrl = _matchMetaTag(html); if(metaUrl != null) { return _resolveXri(metaUrl, callback, hops + 1); } var provider = _matchLinkTag(html, 'openid2.provider'); if(provider == null) { provider = _matchLinkTag(html, 'openid.server'); if(provider == null) { callback(null); } else { var localId = _matchLinkTag(html, 'openid.delegate'); callback([{ version: 'http://openid.net/signon/1.1', endpoint: provider, claimedIdentifier: htmlUrl, localIdentifier : localId }]); } } else { var localId = _matchLinkTag(html, 'openid2.local_id'); callback([{ version: 'http://specs.openid.net/auth/2.0/signon', endpoint: provider, claimedIdentifier: htmlUrl, localIdentifier : localId }]); } } var _parseHostMeta = function(hostMeta, callback) { var match = /^Link: <([^\n\r]+)>;/.exec(hostMeta); if(match != null) { var xriUrl = match[0].slice(7,match.length - 4); _resolveXri(xriUrl, callback); } else { callback(null) } } var _resolveXri = function(xriUrl, callback, hops) { if(!hops) { hops = 1; } else if(hops >= 5) { return callback(null); } _get(xriUrl, null, function(data, headers, statusCode) { if(statusCode != 200) { return callback(null); } var xrdsLocation = headers['x-xrds-location']; if(_isDef(xrdsLocation)) { _get(xrdsLocation, null, function(data, headers, statusCode) { if(statusCode != 200 || data == null) { callback(null); } else { callback(_parseXrds(xrdsLocation, data)); } }); } else if(data != null) { var contentType = headers['content-type']; // text/xml is not compliant, but some hosting providers refuse header // changes, so text/xml is encountered if(contentType.indexOf('application/xrds+xml') === 0 || contentType.indexOf('text/xml') === 0) { return callback(_parseXrds(xriUrl, data)); } else { return _resolveHtml(xriUrl, callback, hops + 1, data); } } }); } var _resolveHtml = function(identifier, callback, hops, data) { if(!hops) { hops = 1; } else if(hops >= 5) { return callback(null); } if(data == null) { _get(identifier, null, function(data, headers, statusCode) { if(statusCode != 200 || data == null) { callback(null); } else { _parseHtml(identifier, data, callback, hops + 1); } }); } else { _parseHtml(identifier, data, callback, hops); } } var _resolveHostMeta = function(identifier, strict, callback, fallBackToProxy) { var host = url.parse(identifier); var hostMetaUrl; if(fallBackToProxy && !strict) { hostMetaUrl = 'https://www.google.com/accounts/o8/.well-known/host-meta?hd=' + host.host } else { hostMetaUrl = host.protocol + '//' + host.host + '/.well-known/host-meta'; } if(!hostMetaUrl) { callback(null); } else { _get(hostMetaUrl, null, function(data, headers, statusCode) { if(statusCode != 200 || data == null) { if(!fallBackToProxy && !strict){ _resolveHostMeta(identifier, strict, callback, true); } else{ callback(null); } } else { //Attempt to parse the data but if this fails it may be because //the response to hostMetaUrl was some other http/html resource. //Therefore fallback to the proxy if no providers are found. _parseHostMeta(data, function(providers){ if((providers == null || providers.length == 0) && !fallBackToProxy && !strict){ _resolveHostMeta(identifier, strict, callback, true); } else{ callback(providers); } }); } }); } } openid.discover = function(identifier, strict, callback) { identifier = _normalizeIdentifier(identifier); if(!identifier) { return callback({ message: 'Invalid identifier' }, null); } if(identifier.indexOf('http') !== 0) { // XRDS identifier = 'https://xri.net/' + identifier + '?_xrd_r=application/xrds%2Bxml'; } // Try XRDS/Yadis discovery _resolveXri(identifier, function(providers) { if(providers == null || providers.length == 0) { // Fallback to HTML discovery _resolveHtml(identifier, function(providers) { if(providers == null || providers.length == 0){ _resolveHostMeta(identifier, strict, function(providers){ callback(null, providers); }); } else{ callback(null, providers); } }); } else { // Add claimed identifier to providers with local identifiers // and OpenID 1.0/1.1 providers to ensure correct resolution // of identities and services for(var i = 0, len = providers.length; i < len; ++i) { var provider = providers[i]; if(!provider.claimedIdentifier && (provider.localIdentifier || provider.version.indexOf('2.0') === -1)) { provider.claimedIdentifier = identifier; } } callback(null, providers); } }); } var _createDiffieHellmanKeyExchange = function(algorithm) { var defaultPrime = 'ANz5OguIOXLsDhmYmsWizjEOHTdxfo2Vcbt2I3MYZuYe91ouJ4mLBX+YkcLiemOcPym2CBRYHNOyyjmG0mg3BVd9RcLn5S3IHHoXGHblzqdLFEi/368Ygo79JRnxTkXjgmY0rxlJ5bU1zIKaSDuKdiI+XUkKJX8Fvf8W8vsixYOr'; var dh = crypto.createDiffieHellman(defaultPrime, 'base64'); dh.generateKeys(); return dh; } openid.associate = function(provider, callback, strict, algorithm) { var params = _generateAssociationRequestParameters(provider.version, algorithm); if(!_isDef(algorithm)) { algorithm = 'DH-SHA256'; } var dh = null; if(algorithm.indexOf('no-encryption') === -1) { dh = _createDiffieHellmanKeyExchange(algorithm); params['openid.dh_modulus'] = _toBase64(dh.getPrime('binary')); params['openid.dh_gen'] = _toBase64(dh.getGenerator('binary')); params['openid.dh_consumer_public'] = _toBase64(dh.getPublicKey('binary')); } _post(provider.endpoint, params, function(data, headers, statusCode) { if ((statusCode != 200 && statusCode != 400) || data === null) { return callback({ message: 'HTTP request failed' }, { error: 'HTTP request failed', error_code: '' + statusCode, ns: 'http://specs.openid.net/auth/2.0' }); } data = _decodePostData(data); if(data.error_code == 'unsupported-type' || !_isDef(data.ns)) { if(algorithm == 'DH-SHA1') { if(strict && url.protocol != 'https:') { return callback({ message: 'Channel is insecure and no encryption method is supported by provider' }, null); } else { return openid.associate(provider, callback, strict, 'no-encryption-256'); } } else if(algorithm == 'no-encryption-256') { if(strict && url.protocol != 'https:') { return callback('Channel is insecure and no encryption method is supported by provider', null); } /*else if(provider.version.indexOf('2.0') === -1) { // 2011-07-22: This is an OpenID 1.0/1.1 provider which means // HMAC-SHA1 has already been attempted with a blank session // type as per the OpenID 1.0/1.1 specification. // (See http://openid.net/specs/openid-authentication-1_1.html#mode_associate) // However, providers like wordpress.com don't follow the // standard and reject these requests, but accept OpenID 2.0 // style requests without a session type, so we have to give // those a shot as well. callback({ message: 'Provider is OpenID 1.0/1.1 and does not support OpenID 1.0/1.1 association.' }); }*/ else { return openid.associate(provider, callback, strict, 'no-encryption'); } } else if(algorithm == 'DH-SHA256') { return openid.associate(provider, callback, strict, 'DH-SHA1'); } } if (data.error) { callback({ message: data.error }, data); } else { var secret = null; var hashAlgorithm = algorithm.indexOf('256') !== -1 ? 'sha256' : 'sha1'; if(algorithm.indexOf('no-encryption') !== -1) { secret = data.mac_key; } else { var serverPublic = _fromBase64(data.dh_server_public); var sharedSecret = convert.btwoc(dh.computeSecret(serverPublic, 'binary', 'binary')); var hash = crypto.createHash(hashAlgorithm); hash.update(sharedSecret); sharedSecret = hash.digest('binary'); var encMacKey = convert.base64.decode(data.enc_mac_key); secret = convert.base64.encode(_xor(encMacKey, sharedSecret)); } if (!_isDef(data.assoc_handle)) { return callback({ message: 'OpenID provider does not seem to support association; you need to use stateless mode'}, null); } openid.saveAssociation(provider, hashAlgorithm, data.assoc_handle, secret, data.expires_in * 1, function(error) { if(error) { return callback(error); } callback(null, data); }); } }); } var _generateAssociationRequestParameters = function(version, algorithm) { var params = { 'openid.mode' : 'associate', }; if(version.indexOf('2.0') !== -1) { params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; } if(algorithm == 'DH-SHA1') { params['openid.assoc_type'] = 'HMAC-SHA1'; params['openid.session_type'] = 'DH-SHA1'; } else if(algorithm == 'no-encryption-256') { if(version.indexOf('2.0') === -1) { params['openid.session_type'] = ''; // OpenID 1.0/1.1 requires blank params['openid.assoc_type'] = 'HMAC-SHA1'; } else { params['openid.session_type'] = 'no-encryption'; params['openid.assoc_type'] = 'HMAC-SHA256'; } } else if(algorithm == 'no-encryption') { if(version.indexOf('2.0') !== -1) { params['openid.session_type'] = 'no-encryption'; } params['openid.assoc_type'] = 'HMAC-SHA1'; } else { params['openid.assoc_type'] = 'HMAC-SHA256'; params['openid.session_type'] = 'DH-SHA256'; } return params; } openid.authenticate = function(identifier, returnUrl, realm, immediate, stateless, callback, extensions, strict) { openid.discover(identifier, strict, function(error, providers) { if(error) { return callback(error); } if(!providers || providers.length === 0) { return callback({ message: 'No providers found for the given identifier' }, null); } var providerIndex = -1; var chooseProvider = function successOrNext(error, authUrl) { if(!error && authUrl) { var provider = providers[providerIndex]; if(provider.claimedIdentifier) { var useLocalIdentifierAsKey = provider.version.indexOf('2.0') === -1 && provider.localIdentifier && provider.claimedIdentifier != provider.localIdentifier; return openid.saveDiscoveredInformation(useLocalIdentifierAsKey ? provider.localIdentifier : provider.claimedIdentifier, provider, function(error) { if(error) { return callback(error); } return callback(null, authUrl); }); } else if(provider.version.indexOf('2.0') !== -1) { return callback(null, authUrl); } else { successOrNext({ message: 'OpenID 1.0/1.1 provider cannot be used without a claimed identifier' }); } } if(++providerIndex >= providers.length) { return callback({ message: 'No usable providers found for the given identifier' }, null); } var currentProvider = providers[providerIndex]; if(stateless) { _requestAuthentication(currentProvider, null, returnUrl, realm, immediate, extensions || {}, successOrNext); } else { openid.associate(currentProvider, function(error, answer) { if(error || !answer || answer.error) { successOrNext(error || answer.error, null); } else { _requestAuthentication(currentProvider, answer.assoc_handle, returnUrl, realm, immediate, extensions || {}, successOrNext); } }); } }; chooseProvider(); }); } var _requestAuthentication = function(provider, assoc_handle, returnUrl, realm, immediate, extensions, callback) { var params = { 'openid.mode' : immediate ? 'checkid_immediate' : 'checkid_setup' }; if(provider.version.indexOf('2.0') !== -1) { params['openid.ns'] = 'http://specs.openid.net/auth/2.0'; } for (var i in extensions) { if(!extensions.hasOwnProperty(i)) { continue; } var extension = extensions[i]; for (var key in extension.requestParams) { if (!extension.requestParams.hasOwnProperty(key)) { continue; } params[key] = extension.requestParams[key]; } } if(provider.claimedIdentifier) { params['openid.claimed_id'] = provider.claimedIdentifier; if(provider.localIdentifier) { params['openid.identity'] = provider.localIdentifier; } else { params['openid.identity'] = provider.claimedIdentifier; } } else if(provider.version.indexOf('2.0') !== -1) { params['openid.claimed_id'] = params['openid.identity'] = 'http://specs.openid.net/auth/2.0/identifier_select'; } else { return callback({ message: 'OpenID 1.0/1.1 provider cannot be used without a claimed identifier' }); } if(assoc_handle) { params['openid.assoc_handle'] = assoc_handle; } if(returnUrl) { // Value should be missing if RP does not want // user to be sent back params['openid.return_to'] = returnUrl; } if(realm) { if(provider.version.indexOf('2.0') !== -1) { params['openid.realm'] = realm; } else { params['openid.trust_root'] = realm; } } else if(!returnUrl) { return callback({ message: 'No return URL or realm specified' }); } callback(null, _buildUrl(provider.endpoint, params)); } openid.verifyAssertion = function(requestOrUrl, callback, stateless, extensions, strict) { extensions = extensions || {}; var assertionUrl = requestOrUrl; if(typeof(requestOrUrl) !== typeof('')) { if(requestOrUrl.method == 'POST') { if(requestOrUrl.headers['content-type'] == 'application/x-www-form-urlencoded') { // POST response received var data = ''; requestOrUrl.on('data', function(chunk) { data += chunk; }); requestOrUrl.on('end', function() { var params = querystring.parse(data); return _verifyAssertionData(params, callback, stateless, extensions, strict); }); } else { return callback({ message: 'Invalid POST response from OpenID provider' }); } return; // Avoid falling through to GET method assertion } else if(requestOrUrl.method != 'GET') { return callback({ message: 'Invalid request method from OpenID provider' }); } assertionUrl = requestOrUrl.url; } assertionUrl = url.parse(assertionUrl, true); var params = assertionUrl.query; return _verifyAssertionData(params, callback, stateless, extensions, strict); } var _verifyAssertionData = function(params, callback, stateless, extensions, strict) { var assertionError = _getAssertionError(params); if(assertionError) { return callback({ message: assertionError }, { authenticated: false }); } if (!_invalidateAssociationHandleIfRequested(params)) { return callback({ message: 'Unable to invalidate association handle'}); } // TODO: Check nonce if OpenID 2.0 _verifyDiscoveredInformation(params, stateless, extensions, strict, function(error, result) { return callback(error, result); }); }; var _getAssertionError = function(params) { if(!_isDef(params)) { return 'Assertion request is malformed'; } else if(params['openid.mode'] == 'error') { return params['openid.error']; } else if(params['openid.mode'] == 'cancel') { return 'Authentication cancelled'; } return null; } var _invalidateAssociationHandleIfRequested = function(params) { if (params['is_valid'] == 'true' && _isDef(params['openid.invalidate_handle'])) { if(!openid.removeAssociation(params['openid.invalidate_handle'])) { return false; } } return true; } var _verifyDiscoveredInformation = function(params, stateless, extensions, strict, callback) { var claimedIdentifier = params['openid.claimed_id']; var useLocalIdentifierAsKey = false; if(!_isDef(claimedIdentifier)) { if(!_isDef(params['openid.ns'])) { // OpenID 1.0/1.1 response without a claimed identifier // We need to load discovered information using the // local identifier useLocalIdentifierAsKey = true; } else { // OpenID 2.0+: // If there is no claimed identifier, then the // assertion is not about an identity return callback(null, { authenticated: false }); } } if (useLocalIdentifierAsKey) { claimedIdentifier = params['openid.identity']; } claimedIdentifier = _getCanonicalClaimedIdentifier(claimedIdentifier); openid.loadDiscoveredInformation(claimedIdentifier, function(error, provider) { if(error) { return callback({ message: 'An error occured when loading previously discovered information about the claimed identifier' }); } if(provider) { return _verifyAssertionAgainstProviders([provider], params, stateless, extensions, callback); } else if (useLocalIdentifierAsKey) { return callback({ message: 'OpenID 1.0/1.1 response received, but no information has been discovered about the provider. It is likely that this is a fraudulent authentication response.' }); } openid.discover(claimedIdentifier, strict, function(error, providers) { if(error) { return callback(error); } if(!providers || !providers.length) { return callback({ message: 'No OpenID provider was discovered for the asserted claimed identifier' }); } _verifyAssertionAgainstProviders(providers, params, stateless, extensions, callback); }); }); } var _verifyAssertionAgainstProviders = function(providers, params, stateless, extensions, callback) { for(var i = 0; i < providers.length; ++i) { var provider = providers[i]; if(!provider.version || provider.version.indexOf(params['openid.ns']) !== 0) { continue; } if(provider.version.indexOf('2.0') !== -1) { var endpoint = params['openid.op_endpoint']; if (provider.endpoint != endpoint) { continue; } if(provider.claimedIdentifier) { var claimedIdentifier = _getCanonicalClaimedIdentifier(params['openid.claimed_id']); if(provider.claimedIdentifier != claimedIdentifier) { return callback({ message: 'Claimed identifier in assertion response does not match discovered claimed identifier' }); } } } if(provider.localIdentifier && provider.localIdentifier != params['openid.identity']) { return callback({ message: 'Identity in assertion response does not match discovered local identifier' }); } return _checkSignature(params, provider, stateless, function(error, result) { if(error) { return callback(error); } if(extensions && result.authenticated) { for(var ext in extensions) { if (!extensions.hasOwnProperty(ext)) { continue; } var instance = extensions[ext]; instance.fillResult(params, result); } } return callback(null, result); }); } callback({ message: 'No valid providers were discovered for the asserted claimed identifier' }); } var _checkSignature = function(params, provider, stateless, callback) { if(!_isDef(params['openid.signed']) || !_isDef(params['openid.sig'])) { return callback({ message: 'No signature in response' }, { authenticated: false }); } if(stateless) { _checkSignatureUsingProvider(params, provider, callback); } else { _checkSignatureUsingAssociation(params, callback); } } var _checkSignatureUsingAssociation = function(params, callback) { if (!_isDef(params['openid.assoc_handle'])) { return callback({ message: 'No association handle in provider response. Find out whether the provider supports associations and/or use stateless mode.' }); } openid.loadAssociation(params['openid.assoc_handle'], function(error, association) { if(error) { return callback({ message: 'Error loading association' }, { authenticated: false }); } if(!association) { return callback({ message:'Invalid association handle' }, { authenticated: false }); } if(association.provider.version.indexOf('2.0') !== -1 && association.provider.endpoint !== params['openid.op_endpoint']) { return callback({ message:'Association handle does not match provided endpoint' }, {authenticated: false}); } var message = ''; var signedParams = params['openid.signed'].split(','); for(var i = 0; i < signedParams.length; i++) { var param = signedParams[i]; var value = params['openid.' + param]; if(!_isDef(value)) { return callback({ message: 'At least one parameter referred in signature is not present in response' }, { authenticated: false }); } message += param + ':' + value + '\n'; } var hmac = crypto.createHmac(association.type, convert.base64.decode(association.secret)); hmac.update(message, 'utf8'); var ourSignature = hmac.digest('base64'); if(ourSignature == params['openid.sig']) { callback(null, { authenticated: true, claimedIdentifier: association.provider.version.indexOf('2.0') !== -1 ? params['openid.claimed_id'] : association.provider.claimedIdentifier }); } else { callback({ message: 'Invalid signature' }, { authenticated: false }); } }); } var _checkSignatureUsingProvider = function(params, provider, callback) { var requestParams = { 'openid.mode' : 'check_authentication' }; for(var key in params) { if(params.hasOwnProperty(key) && key != 'openid.mode') { requestParams[key] = params[key]; } } _post(_isDef(params['openid.ns']) ? (params['openid.op_endpoint'] || provider.endpoint) : provider.endpoint, requestParams, function(data, headers, statusCode) { if(statusCode != 200 || data == null) { return callback({ message: 'Invalid assertion response from provider' }, { authenticated: false }); } else { data = _decodePostData(data); if(data['is_valid'] == 'true') { return callback(null, { authenticated: true, claimedIdentifier: provider.version.indexOf('2.0') !== -1 ? params['openid.claimed_id'] : params['openid.identity'] }); } else { return callback({ message: 'Invalid signature' }, { authenticated: false }); } } }); } var _getCanonicalClaimedIdentifier = function(claimedIdentifier) { if(!claimedIdentifier) { return claimedIdentifier; } var index = claimedIdentifier.indexOf('#'); if (index !== -1) { return claimedIdentifier.substring(0, index); } return claimedIdentifier; }; /* ================================================================== * Extensions * ================================================================== */ var _getExtensionAlias = function(params, ns) { for (var k in params) if (params[k] == ns) return k.replace("openid.ns.", ""); } /* * Simple Registration Extension * http://openid.net/specs/openid-simple-registration-extension-1_1-01.html */ var sreg_keys = ['nickname', 'email', 'fullname', 'dob', 'gender', 'postcode', 'country', 'language', 'timezone']; openid.SimpleRegistration = function SimpleRegistration(options) { this.requestParams = {'openid.ns.sreg': 'http://openid.net/extensions/sreg/1.1'}; if (options.policy_url) this.requestParams['openid.sreg.policy_url'] = options.policy_url; var required = []; var optional = []; for (var i = 0; i < sreg_keys.length; i++) { var key = sreg_keys[i]; if (options[key]) { if (options[key] == 'required') { required.push(key); } else { optional.push(key); } } if (required.length) { this.requestParams['openid.sreg.required'] = required.join(','); } if (optional.length) { this.requestParams['openid.sreg.optional'] = optional.join(','); } } }; openid.SimpleRegistration.prototype.fillResult = function(params, result) { var extension = _getExtensionAlias(params, 'http://openid.net/extensions/sreg/1.1') || 'sreg'; for (var i = 0; i < sreg_keys.length; i++) { var key = sreg_keys[i]; if (params['openid.' + extension + '.' + key]) { result[key] = params['openid.' + extension + '.' + key]; } } }; /* * User Interface Extension * http://svn.openid.net/repos/specifications/user_interface/1.0/trunk/openid-user-interface-extension-1_0.html */ openid.UserInterface = function UserInterface(options) { if (typeof(options) != 'object') { options = { mode: options || 'popup' }; } this.requestParams = {'openid.ns.ui': 'http://specs.openid.net/extensions/ui/1.0'}; for (var k in options) { this.requestParams['openid.ui.' + k] = options[k]; } }; openid.UserInterface.prototype.fillResult = function(params, result) { // TODO: Fill results } /* * Attribute Exchange Extension * http://openid.net/specs/openid-attribute-exchange-1_0.html * Also see: * - http://www.axschema.org/types/ * - http://code.google.com/intl/en-US/apis/accounts/docs/OpenID.html#Parameters */ // TODO: count handling var attributeMapping = { 'http://axschema.org/contact/country/home': 'country' , 'http://axschema.org/contact/email': 'email' , 'http://axschema.org/namePerson/first': 'firstname' , 'http://axschema.org/pref/language': 'language' , 'http://axschema.org/namePerson/last': 'lastname' // The following are not in the Google document: , 'http://axschema.org/namePerson/friendly': 'nickname' , 'http://axschema.org/namePerson': 'fullname' }; openid.AttributeExchange = function AttributeExchange(options) { this.requestParams = {'openid.ns.ax': 'http://openid.net/srv/ax/1.0', 'openid.ax.mode' : 'fetch_request'}; var required = []; var optional = []; for (var ns in options) { if (!options.hasOwnProperty(ns)) { continue; } if (options[ns] == 'required') { required.push(ns); } else { optional.push(ns); } } var self = this; required = required.map(function(ns, i) { var attr = attributeMapping[ns] || 'req' + i; self.requestParams['openid.ax.type.' + attr] = ns; return attr; }); optional = optional.map(function(ns, i) { var attr = attributeMapping[ns] || 'opt' + i; self.requestParams['openid.ax.type.' + attr] = ns; return attr; }); if (required.length) { this.requestParams['openid.ax.required'] = required.join(','); } if (optional.length) { this.requestParams['openid.ax.if_available'] = optional.join(','); } } openid.AttributeExchange.prototype.fillResult = function(params, result) { var extension = _getExtensionAlias(params, 'http://openid.net/srv/ax/1.0') || 'ax'; var regex = new RegExp('^openid\\.' + extension + '\\.(value|type)\\.(\\w+)$'); var aliases = {}; var values = {}; for (var k in params) { if (!params.hasOwnProperty(k)) { continue; } var matches = k.match(regex); if (!matches) { continue; } if (matches[1] == 'type') { aliases[params[k]] = matches[2]; } else { values[matches[2]] = params[k]; } } for (var ns in aliases) { if (aliases[ns] in values) { result[aliases[ns]] = values[aliases[ns]]; result[ns] = values[aliases[ns]]; } } } openid.OAuthHybrid = function(options) { this.requestParams = { 'openid.ns.oauth' : 'http://specs.openid.net/extensions/oauth/1.0', 'openid.oauth.consumer' : options['consumerKey'], 'openid.oauth.scope' : options['scope']}; } openid.OAuthHybrid.prototype.fillResult = function(params, result) { var extension = _getExtensionAlias(params, 'http://specs.openid.net/extensions/oauth/1.0') || 'oauth' , token_attr = 'openid.' + extension + '.request_token'; if(params[token_attr] !== undefined) { result['request_token'] = params[token_attr]; } }; /* * Provider Authentication Policy Extension (PAPE) * http://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html * * Note that this extension does not validate that the provider is obeying the * authentication request, it only allows the request to be made. * * TODO: verify requested 'max_auth_age' against response 'auth_time' * TODO: verify requested 'auth_level.ns.' (etc) against response 'auth_level.ns.' * TODO: verify requested 'preferred_auth_policies' against response 'auth_policies' * */ /* Just the keys that aren't open to customisation */ var pape_request_keys = ['max_auth_age', 'preferred_auth_policies', 'preferred_auth_level_types' ]; var pape_response_keys = ['auth_policies', 'auth_time'] /* Some short-hand mappings for auth_policies */ var papePolicyNameMap = { 'phishing-resistant': 'http://schemas.openid.net/pape/policies/2007/06/phishing-resistant', 'multi-factor': 'http://schemas.openid.net/pape/policies/2007/06/multi-factor', 'multi-factor-physical': 'http://schemas.openid.net/pape/policies/2007/06/multi-factor-physical', 'none' : 'http://schemas.openid.net/pape/policies/2007/06/none' } openid.PAPE = function PAPE(options) { this.requestParams = {'openid.ns.pape': 'http://specs.openid.net/extensions/pape/1.0'}; for (var k in options) { if (k === 'preferred_auth_policies') { this.requestParams['openid.pape.' + k] = _getLongPolicyName(options[k]); } else { this.requestParams['openid.pape.' + k] = options[k]; } } var util = require('util'); }; /* you can express multiple pape 'preferred_auth_policies', so replace each * with the full policy URI as per papePolicyNameMapping. */ var _getLongPolicyName = function(policyNames) { var policies = policyNames.split(' '); for (var i=0; i= 0.6.0" ], "devDependencies": { "nodeunit": "~0.8.4", "sinon": "~1.8.1" } } node-openid-0.5.9/sample.js000066400000000000000000000120621232655271400155550ustar00rootroot00000000000000/* A simple sample demonstrating OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN */ var openid = require('./openid'); var url = require('url'); var querystring = require('querystring'); var extensions = [new openid.UserInterface(), new openid.SimpleRegistration( { "nickname" : true, "email" : true, "fullname" : true, "dob" : true, "gender" : true, "postcode" : true, "country" : true, "language" : true, "timezone" : true }), new openid.AttributeExchange( { "http://axschema.org/contact/email": "required", "http://axschema.org/namePerson/friendly": "required", "http://axschema.org/namePerson": "required" }), new openid.PAPE( { "max_auth_age": 24 * 60 * 60, // one day "preferred_auth_policies" : "none" //no auth method preferred. })]; var relyingParty = new openid.RelyingParty( 'http://example.com/verify', // Verification URL (yours) null, // Realm (optional, specifies realm for OpenID authentication) false, // Use stateless verification false, // Strict mode extensions); // List of extensions to enable and include var server = require('http').createServer( function(req, res) { var parsedUrl = url.parse(req.url); if(parsedUrl.pathname == '/authenticate') { // User supplied identifier var query = querystring.parse(parsedUrl.query); var identifier = query.openid_identifier; // Resolve identifier, associate, and build authentication URL relyingParty.authenticate(identifier, false, function(error, authUrl) { if(error) { res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end('Authentication failed: ' + error.message); } else if (!authUrl) { res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8' }); res.end('Authentication failed'); } else { res.writeHead(302, { Location: authUrl }); res.end(); } }); } else if(parsedUrl.pathname == '/verify') { // Verify identity assertion // NOTE: Passing just the URL is also possible relyingParty.verifyAssertion(req, function(error, result) { res.writeHead(200, { 'Content-Type' : 'text/plain; charset=utf-8' }); if(error) { res.end('Authentication failed: ' + error.message); } else { // Result contains properties: // - authenticated (true/false) // - answers from any extensions (e.g. // "http://axschema.org/contact/email" if requested // and present at provider) res.end((result.authenticated ? 'Success :)' : 'Failure :(') + '\n\n' + JSON.stringify(result)); } }); } else { // Deliver an OpenID form on all other URLs res.writeHead(200, { 'Content-Type' : 'text/html; charset=utf-8' }); res.end('' + '
' + '

Login using OpenID

' + '' + '' + '
'); } }); server.listen(80); node-openid-0.5.9/test/000077500000000000000000000000001232655271400147145ustar00rootroot00000000000000node-openid-0.5.9/test/openid_fast_tests.js000066400000000000000000000071311232655271400207710ustar00rootroot00000000000000/* OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN */ var openid = require('../openid'); exports.testVerificationUrl = function(test) { var times = 0; openid.verifyAssertion('http://fu', function(error, result) { test.ok(!times++); test.ok(!result || !result.authenticated); test.done(); }); } exports.testVerificationCancel = function(test) { var times = 0; openid.verifyAssertion( 'http://host/?openid.mode=cancel' + '&openid.ns=http%3A%2F%2Fspecs.openid.net%2Fauth%2F2.0', function(error, result) { test.ok(!times++); test.ok(!result || !result.authenticated); test.done(); }); } exports.testVerificationUrlUsingRelyingParty = function(test) { var rp = new openid.RelyingParty( 'http://example.com/verify', null, false, false, null); rp.verifyAssertion('http://fu', function(error, result) { test.ok(!result || !result.authenticated); test.done(); }); } exports.testAttributeExchange = function(test) { var ax = new openid.AttributeExchange(), results = {}, exampleParams = { 'openid.ax.type.email' : 'http://axschema.org/contact/email', 'openid.ax.value.email' : 'fred.example@gmail.com', 'openid.ax.type.language' : 'http://axschema.org/pref/language', 'openid.ax.value.language' : 'english' } ax.fillResult(exampleParams, results); test.notEqual(results['email'], undefined); test.notEqual(results['http://axschema.org/contact/email'], undefined); test.notEqual(results['language'], undefined); test.notEqual(results['http://axschema.org/pref/language'], undefined); test.equal('fred.example@gmail.com', results['email']); test.equal('fred.example@gmail.com', results['http://axschema.org/contact/email']); test.equal('english', results['language']); test.equal('english', results['http://axschema.org/pref/language']); test.done(); } exports.testPape = function(test) { var exampleParams = { "openid.pape.auth_time" : new Date().toISOString(), "openid.pape.auth_policies" : 'http://schemas.openid.net/pape/policies/2007/06/multi-factor http://schemas.openid.net/pape/policies/2007/06/phishing-resistant' }; var pape = new openid.PAPE(), results = {}; pape.fillResult(exampleParams, results); test.notEqual(results['auth_time'], undefined); test.notEqual(results['auth_policies'], undefined); test.equal(results['auth_policies'], "multi-factor phishing-resistant"); test.done(); } node-openid-0.5.9/test/openid_integration_tests.js000066400000000000000000000141151232655271400223570ustar00rootroot00000000000000/* OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN */ var constants = require('constants'); var https = require('https'); var openid = require('../openid'); var sinon = require('sinon'); // OpenSSL seems to have issues with some servers and causes some of the // following test cases to fail in Node 0.10.x unless we disable TLS 1.2. See // https://github.com/joyent/node/issues/5360 https.globalAgent.options.secureOptions = constants.SSL_OP_NO_TLSv1_2; var clock; exports.setUp = function(callback) { // By default the openid module save association data to memory and sets up // a timer to expire stale entries. This timer prevents the test cases from // exiting so we fake the timer implementation during the test run. clock = sinon.useFakeTimers('setTimeout'); callback(); } exports.tearDown = function(callback) { clock.restore(); delete clock; callback(); } exports.testResolveFailed = function(test) { openid.authenticate('example.com', 'http://example.com/verify', null, false, false, function(error, url) { test.ok(error); test.equal(null, url); test.done(); }); } exports.testEmptyUrl = function(test) { openid.discover('', true, function(error, providers) { test.ok(error); test.equal(null, providers); test.done(); }); } exports.testResolveRyanXri = function(test) { openid.discover('=ryan', true, function(error, providers) { test.ok(!error); test.equal(2, providers.length); test.done(); }); } exports.testResolveRedirect = function(test) { openid.discover('http://www.myopenid.com/xrds?username=swatinem.myopenid.com', true, function(error, providers) { test.ok(!error); test.equal(3, providers.length); test.done(); }); } exports.testResolveGoogle = function(test) { openid.discover('http://www.google.com/accounts/o8/id', true, function(error, providers) { test.ok(!error); test.equal(1, providers.length); test.done(); }); } exports.testResolveLiveJournalUser = function(test) { openid.discover('http://omnifarious.livejournal.com/', true, function(error, providers) { test.ok(!error); test.equal(1, providers.length); test.done(); }); } exports.testResolveOpenID11 = function(test) { // FIXME: relying on a third party for back-level protocol support is brittle. openid.discover('http://pupeno.com/', true, function(error, providers) { test.ok(!error); test.notEqual(null, providers); test.equal(1, providers.length); test.equal(providers[0].version, 'http://openid.net/signon/1.1'); test.done(); }); } function associateTest(url, version, test) { if (arguments.length == 2) { test = version; version = null; } openid.discover(url, true, function(error, providers) { var provider = providers[0]; openid.associate(provider, function(error, result) { test.ok(!error); if (version) { test.equal(provider.version, version); } test.ok(result.expires_in); test.done(); }); } ); } exports.testAssociateWithGoogle = function(test) { associateTest('http://www.google.com/accounts/o8/id', test); } exports.testAssociateWithLiveJournal = function(test) { associateTest('http://omnifarious.livejournal.com/', test); } exports.testAssociateWithOpenID11 = function(test) { // FIXME: relying on a third party for back-level protocol support is brittle. associateTest('http://pupeno.com/', 'http://openid.net/signon/1.1', test); } exports.testImmediateAuthenticationWithGoogle = function(test) { openid.authenticate('http://www.google.com/accounts/o8/id', 'http://example.com/verify', null, true, false, function(error, url) { test.ok(!error, error); test.ok(url.indexOf('checkid_immediate') !== -1); test.done(); }); } exports.testImmediateAuthenticationWithGoogleAppsForDomains = function(test) { // domain must be a valid google apps domain with openid enabled. openid.authenticate('https://www.google.com/accounts/o8/site-xrds?hd=opower.com', 'http://example.com/verify', null, true, false, function(error, url) { test.ok(!error, error); test.ok(url.indexOf('checkid_immediate') !== -1); test.done(); }); } exports.testSetupAuthenticationWithGoogle = function(test) { openid.authenticate('http://www.google.com/accounts/o8/id', 'http://example.com/verify', null, false, false, function(error, url) { test.ok(!error); test.ok(url.indexOf('checkid_setup') !== -1); test.done(); }); } exports.testAuthenticationWithGoogleUsingRelyingPartyObject = function(test) { var rp = new openid.RelyingParty( 'http://example.com/verify', null, false, false, null); rp.authenticate('http://www.google.com/accounts/o8/id', false, function(error, url) { test.ok(!error); test.ok(url.indexOf('checkid_setup') !== -1); test.done(); }); } node-openid-0.5.9/test/xrds_tests.js000066400000000000000000000154511232655271400174620ustar00rootroot00000000000000/* OpenID for node.js * * http://ox.no/software/node-openid * http://github.com/havard/node-openid * * Copyright (C) 2010 by Håvard Stranden * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN */ var xrds = require('../lib/xrds'); exports.testXrdsSampleParse = function(test) { var sample = '*example2008-05-05T00:15:00.000Z xri://= !4C72.6C81.D78F.90B2 http://example.com/example-user http://example.net/blog xri://=!4C72.6C81.D78F.90B2 xri://=!F83.62B1.44F.2813 xri://$res*auth*($v*2.0) application/xrds+xml http://resolve.example.com http://resolve2.example.com https://resolve.example.com http://specs.openid.net/auth/2.0/signon http://www.myopenid.com/server http://example.myopenid.com/ http://openid.net/server/1.0 http://www.livejournal.com/openid/server.bml http://www.livejournal.com/users/example/ /media/pictures image/jpeg http://pictures.example.com '; var services = xrds.parse(sample); test.equal(3, services.length); // Won't find service with self-closing type (OK) test.done(); } exports.testXrdsSampleWithCertificateParse = function(test) { var sample = ' *ryan 2010-12-10T00:05:09.000Z xri://= !6211.6637.5b81.1e16 =!6211.6637.5B81.1E16 @!26E.5985.6045.FCED xri://+i-service*(+contact)*($v*1.0) (+contact) http://contact.fullxri.com/contact/ @!26E.5985.6045.FCED xri://+i-service*(+forwarding)*($v*1.0) (+index) http://forwarding.fullxri.com/forwarding/ xri://=!6211.6637.5B81.1E16 xri://$res*auth*($v*2.0) application/xrds+xml http://resolve.fullxri.com/ns/=!6211.6637.5B81.1E16/ https://resolve.fullxri.com/ns/=!6211.6637.5B81.1E16/ @!26E.5985.6045.FCED xri://$xdi!($v!1) ($context)!($xdi)!($v!1) https://xdi.fullxri.com/=!6211.6637.5B81.1E16 xri://$certificate*($x.509) MIIDIzCCAgugAwIBAgIGASCS6gA8MA0GCSqGSIb3DQEBBQUAMIGEMQswCQYDVQQGEwJBVDEPMA0GA1UECBMGVmllbm5hMQ8wDQYDVQQHEwZWaWVubmExETAPBgNVBAoUCEBmdWxsWFJJMR0wGwYDVQQDFBRAITI2RS41OTg1LjYwNDUuRkNFRDEhMB8GCSqGSIb3DQEJARYSb2ZmaWNlQGZ1bGx4cmkuY29tMB4XDTA5MDQxMTAyMDMxMFoXDTI5MDQxMTAyMDMxMFowIDEeMBwGA1UEAwwVPSE2MjExLjY2MzcuNUI4MS4xRTE2MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAo6LCtJ3yvnVo5UPD09jgrC6d94dwu0gqAzAHTjaVXwsEsuelJlC2psugxF970zKaglAxK/tGgW0Ycf7qjaNkhWKmZhSX8/IajQ/eUCqAggddpKLzuUjMvTTRso+WuJpIIsQfsX3WQGaUkOqxrnwHR+zrbIUJKpVCUmkp7oToW0yQMC+HTXOPW/SJrm0nIRcT+b1PN98WacRihPV/mwsNeOUiLUNfo60Apt/jM4T0UqNvfT/QmW+6/4g9+P16580uG9lbh0UOoz+TuVu1Q/cM53anZP2thpxQU0VasHyZMwpXJb8trj/kFRyTHIQ/ATE80ofNR1qTcz/O5jmmVfwKRwIDAQABMA0GCSqGSIb3DQEBBQUAA4IBAQBMpdu45L7PQR8S2vdA+vnMiDAMUoEqDQ3dr9k6lLd4yDacaGtn/w7hPx3Z3hfOt0crHGcH1TJBbA2klv5PFt2SNObn4VnckpSD065yrcRg82ZGHhNl+0ALqoHzmpnp7MxVR2Bywq5IxzGcjlJ5/Rt21hGeqETIXDzoUJM25/gSQACAADrxdzecHDs2HYtfgDTGXEJixxHFuboVgxUb1jgjVQwGAsNSgj+X3xh47z0bbZMsA0ZxekZx6vTNhRgJ3mMuYBo5Z32JZJDP9fVAR4eIYVjcaD81UaEzVFrzsMaGyHEcrKlelIwriCitx2qjzfNBS35LDW8c5+mtZOQukmoy @!26E.5985.6045.FCED http://openid.net/signon/1.0 http://specs.openid.net/auth/2.0/signon (+login) https://authn.fullxri.com/authentication/ '; var services = xrds.parse(sample); test.equal(6, services.length); // Won't find x509 service due to no URI test.done(); }