pax_global_header 0000666 0000000 0000000 00000000064 12326552714 0014521 g ustar 00root root 0000000 0000000 52 comment=d9ccd0f840a085462d1409f4d53b1260501e42fe
node-openid-0.5.9/ 0000775 0000000 0000000 00000000000 12326552714 0013735 5 ustar 00root root 0000000 0000000 node-openid-0.5.9/.gitattributes 0000664 0000000 0000000 00000000153 12326552714 0016627 0 ustar 00root root 0000000 0000000 # Normalize line endings to LF for text files
* text=auto
# Explicitly declare JS files as text
*.js text
node-openid-0.5.9/.gitignore 0000664 0000000 0000000 00000000016 12326552714 0015722 0 ustar 00root root 0000000 0000000 node_modules/
node-openid-0.5.9/LICENSE 0000664 0000000 0000000 00000002050 12326552714 0014737 0 ustar 00root root 0000000 0000000 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
THE SOFTWARE.
node-openid-0.5.9/README.md 0000664 0000000 0000000 00000015162 12326552714 0015221 0 ustar 00root root 0000000 0000000 # 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('
'
+ '');
}
});
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/ 0000775 0000000 0000000 00000000000 12326552714 0017324 5 ustar 00root root 0000000 0000000 node-openid-0.5.9/expressjs_sample/authentication_controller.js 0000664 0000000 0000000 00000002525 12326552714 0025150 0 ustar 00root root 0000000 0000000 var 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.jade 0000664 0000000 0000000 00000000402 12326552714 0021255 0 ustar 00root root 0000000 0000000 doctype 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/ 0000775 0000000 0000000 00000000000 12326552714 0014503 5 ustar 00root root 0000000 0000000 node-openid-0.5.9/lib/base64.js 0000664 0000000 0000000 00000012070 12326552714 0016125 0 ustar 00root root 0000000 0000000 /* 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.js 0000664 0000000 0000000 00000003232 12326552714 0016521 0 ustar 00root root 0000000 0000000 /* 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.js 0000664 0000000 0000000 00000005647 12326552714 0016035 0 ustar 00root root 0000000 0000000 /* 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.js 0000664 0000000 0000000 00000125302 12326552714 0015554 0 ustar 00root root 0000000 0000000 /* 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