Skip to content

Commit

Permalink
Improved the http/digest Authorization header parsing (still not conv…
Browse files Browse the repository at this point in the history
…inced its right).

Added support for facebook's OAuth2 sign-on.
  • Loading branch information
ciaranj committed May 2, 2010
1 parent c0379b0 commit 15a8df6
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 56 deletions.
45 changes: 37 additions & 8 deletions examples/app.js
Expand Up @@ -4,11 +4,12 @@ var sys= require('sys');
kiwi.require('express')
require('express/plugins')
kiwi.seed('oauth')
var OAuth= require('oauth').OAuth;

//require.paths.unshift(__dirname+ "/../lib/node-oauth/lib/")
var OAuth= require('oauth').OAuth;
var OAuth2= require('oauth2').OAuth2;

global.merge(require('../lib/express/plugins/auth'));
Object.merge(global, require('../lib/express/plugins/auth'));

var getPasswordForUserFunction= function(user, callback) {
var result;
Expand All @@ -17,11 +18,18 @@ var getPasswordForUserFunction= function(user, callback) {
}

use(Cookie)
use(Logger)
use(Session, { lifetime: (150).seconds, reapInterval: (10).seconds })

// N.B. TO USE the facebook strategy you must specify these values correctly for your application.
var fbId= "";
var fbSecret= "";


var StrategyDefinition= require('../lib/express/plugins/strategyDefinition').StrategyDefinition;
use(Auth, {strategies:{"anon": new StrategyDefinition(Anonymous),
"never": new StrategyDefinition(Never),
"facebook": new StrategyDefinition(Facebook, {appId : fbId, appSecret: fbSecret, scope: "email"}),
"twitter": new StrategyDefinition(Twitter, {consumerKey: "TOqGJsdtsicNz4FDSW4N5A", consumerSecret: "CN15nhsuAGQVGL3MDAzfJ3F5FFhp1ce9U4ZbaFZrSwA"}),
"http": new StrategyDefinition(Http, {getPasswordForUser: getPasswordForUserFunction}),
"basic": new StrategyDefinition(Basic, {getPasswordForUser: getPasswordForUserFunction}),
Expand All @@ -38,26 +46,47 @@ get ('/twitter', function() {
"1.0",
"HMAC-SHA1");
oa.getProtectedResource("http://twitter.com/statuses/user_timeline.xml", "GET", self.session.auth["oauth_token"], self.session.auth["oauth_token_secret"], function (error, data) {
self.halt(200, "<html><h1>Hello! Twitter authenticated user ("+self.session.auth.user.username+")</h1>"+data+ "</html>")
sys.p('got protected resource ')
self.respond(200, "<html><h1>Hello! Twitter authenticated user ("+self.session.auth.user.username+")</h1>"+data+ "</html>")
});
}
else {
self.halt(200, "<html><h1>Twitter authentication failed :( </h1></html>")
self.respond(200, "<html><h1>Twitter authentication failed :( </h1></html>")
}
});
})

get ('/facebook', function() {
var self=this;
require('sys').puts('/facebook')
self.authenticate(['facebook'], function(error, authenticated) {
if( authenticated ) {

self.respond(200, "<html><h1>Hello Facebook user:" + JSON.stringify( self.session.auth.user ) + ".</h1></html>")
}
else {
self.respond(200, "<html><h1>Twitter authentication failed :( </h1></html>")
}
});
})

get('/anon', function() {
var self=this;
self.authenticate(['anon'], function(error, authenticated) {
self.halt(200, "<html><h1>Hello! Full anonymous access</h1></html>")
self.respond(200, "<html><h1>Hello! Full anonymous access</h1></html>")
});
})

get('/digest', function() {
var self=this;
self.authenticate(['digest'], function(error, authenticated) {
self.halt(200, "<html><h1>Hello! My little digestive"+ self.session.auth.user.username+ "</h1>" + "<p>" + (self.session.counter++) +"</p></html>")
if( authenticated ) {
if( ! self.session.counter ) self.session.counter= 0;
self.respond(200, "<html><h1>Hello! My little digestive"+ self.session.auth.user.username+ "</h1>" + "<p>" + (self.session.counter++) +"</p></html>")
}
else {
self.respond(200, "<html><h1>should not be happening...</h1></html>")
}
});
})

Expand All @@ -66,10 +95,10 @@ get('/', function() {
self.authenticate(['never', 'digest', 'anon'], function(error, authenticated) {
if( authenticated ) {
if( ! self.session.counter ) self.session.counter= 0;
self.halt(200, "<html><h1>Hello!"+ self.session.auth.user.username+ "</h1>" + "<p>" + (self.session.counter++) +"</p></html>")
self.respond(200, "<html><h1>Hello!"+ self.session.auth.user.username+ "</h1>" + "<p>" + (self.session.counter++) +"</p></html>")
}
else {
self.halt(200, "<html><h1>Who are you, you seem to be un-authenticateable</h1></html>")
self.respond(200, "<html><h1>Who are you, you seem to be un-authenticateable</h1></html>")
}
});
})
Expand Down
21 changes: 11 additions & 10 deletions lib/express/plugins/auth.js
@@ -1,17 +1,18 @@
exports.merge(require('./auth.strategy.base'))
Object.merge(exports, require('./auth.strategy.base'))
AuthStrategy= exports.AuthStrategy
BaseHttpStrategy= require('./auth.strategies/http/base').BaseHttpStrategy
exports.merge(require('./auth.strategies/http/digest'))
exports.merge(require('./auth.strategies/http/basic'))
exports.merge(require('./auth.strategies/http/http'))
exports.merge(require('./auth.strategies/anonymous'))
exports.merge(require('./auth.strategies/never'))
exports.merge(require('./auth.strategies/twitter'))
Object.merge(exports, require('./auth.strategies/http/digest'))
Object.merge(exports, require('./auth.strategies/http/basic'))
Object.merge(exports, require('./auth.strategies/http/http'))
Object.merge(exports, require('./auth.strategies/anonymous'))
Object.merge(exports, require('./auth.strategies/never'))
Object.merge(exports, require('./auth.strategies/twitter'))
Object.merge(exports, require('./auth.strategies/facebook'))

exports.merge(require('./strategies'))
exports.merge(require('./strategyExecutor'))
Object.merge(exports, require('./strategies'))
Object.merge(exports, require('./strategyExecutor'))

Http= exports.Http
Never= exports.Never
Anonymous= exports.Anonymous
exports.merge(require('./auth.core'))
Object.merge(exports, require('./auth.core'))
7 changes: 3 additions & 4 deletions lib/express/plugins/auth.strategies/http/base.js
Expand Up @@ -6,14 +6,13 @@ exports.BaseHttpStrategy= AuthStrategy.extend({
},

_badRequest: function ( request, callback ) {
request.halt(400, 'Bad Request');
request.respond(400, 'Bad Request');
this.halt(callback);
},

_unAuthenticated: function( request, callback ) {
request.header('WWW-Authenticate', this.getAuthenticateResponseHeader());
request.halt(401, "Authorization Required");
request.respond(401, "Authorization Required");
this.halt(callback);
},

}
});
112 changes: 83 additions & 29 deletions lib/express/plugins/auth.strategies/http/digest.js
@@ -1,52 +1,106 @@
var md5= require('support/ext/lib/ext/md5'),
utils = require('express/utils');

var sys= require('sys');
BaseHttpStrategy= require('./base').BaseHttpStrategy;

exports.Digest= BaseHttpStrategy.extend({
constructor: function(options){
var options= options || {}
BaseHttpStrategy.prototype.constructor.call(this, options)
this._realm= options.realm || "test"
this._realm= options.realm || "secure"
this._getPasswordForUser= options.getPasswordForUser;
},

authenticate: function(request, callback) {
var self= this;
var authHeader= request.header('Authorization');
// Digest username="foo", realm="test", nonce="b343d03296358b5d7f985500568b", uri="/", response="52bc08c966a3b16bedb62f1b4a5b40f8"
//TODO: parse this properly, temporary regex hack.
var isDigest= /^[D]igest\s.+"/.exec(authHeader)
var username= /^[D]igest\susername="(.+?)"/.exec(authHeader)
var response= /^[D]igest.+?response="(.+?)"/.exec(authHeader)
var nonce= /^[D]igest.+?nonce="(.+?)"/.exec(authHeader)
if( isDigest && username && username[1] && response && response[1] && nonce && nonce[1]) {
nonce= nonce[1];
username= username[1];
response= response[1];
}
var method= request.method
var href= request.url.href
if(authHeader) {
var credentials= this._splitAuthorizationHeader(authHeader);
var method= request.method
var href= request.url.href


this._getPasswordForUser(username, function(error, password){
if(error) callback(error);
else {
var HA1= md5.hash( username+":"+ self._realm + ":"+ password)
var HA2= md5.hash( method+ ":" + href )
var myResponse= md5.hash(HA1 + ":"+ nonce + ":"+ HA2 )
if( myResponse == response ) {
self.success({"username":username}, callback);
}
this._getPasswordForUser(credentials.username, function(error, password){
if(error) callback(error);
else {
self._unAuthenticated(request, callback)
var HA1= md5.hash( credentials.username+":"+ self._realm + ":"+ password)
var HA2= md5.hash( method+ ":" + href )
var myResponse= md5.hash(HA1 + ":"+ credentials.nonce + ":"+ HA2 )
if( myResponse == credentials.response ) {
self.success({ username : credentials.username}, callback);
}
else {
self._unAuthenticated(request, callback)
}
}
}
})
})
}
else {
self._unAuthenticated(request, callback)
}

},

getAuthenticateResponseHeader: function( request ) {
return "Digest realm=" + this._realm + ", nonce="+ utils.uid();
return "Digest realm=\"" + this._realm.replace("\"","\\\"") + "\", nonce=\""+ this._getNonce(32)+"\"";
},

/**
* Given a valid Digest Authorization HTTP Header will return an object literal
* that contains the passed credentials.
*
* @return {object} The digest credentials, un-encoded and un-quoted.
* @api private
*/
_splitAuthorizationHeader: function( authorizationHeader ) {

var results= {};

var parameterPairs= [];
var isInQuotes= false;
var lastStringStartingBoundary= 0;

//Need to pull off authentication type first
results.type= /^([a-zA-Z]+)\s/.exec(authorizationHeader)[1];
authorizationHeader= authorizationHeader.substring( results.type.length + 1 ) // type + 1 whitespace

for(var i=0;i< authorizationHeader.length;i++) {
if( authorizationHeader[i] == "\"" && authorizationHeader[i-1] != "\\" ) {
// WE've found an un-escaped quote (do escaped quotes exist, need to check the RFC)
isInQuotes= !isInQuotes;
}
if( authorizationHeader[i] == "," && !isInQuotes ) {
var credentialsPart= authorizationHeader.substr(lastStringStartingBoundary, (i-lastStringStartingBoundary));
//Strip whitespace..
credentialsPart= credentialsPart.replace(/^\s+|\s+$/g,'')


parameterPairs[parameterPairs.length]= credentialsPart;
lastStringStartingBoundary= i+1; // skip the comma.
}
}

// Refactor this code.
if( lastStringStartingBoundary < authorizationHeader.length ) {
var credentialsPart= authorizationHeader.substr(lastStringStartingBoundary, (authorizationHeader.length-lastStringStartingBoundary));
//Strip whitespace..
credentialsPart= credentialsPart.replace(/^\s+|\s+$/g,'')
parameterPairs[parameterPairs.length]= credentialsPart;
lastStringStartingBoundary= i+1; // skip the comma.
}


for(var key in parameterPairs) {
var pair= /^(.+)?=(.+)/.exec(parameterPairs[key])

//de-code quotes and un-escape inter-stitial quotes if appropriate
// I'm lost as to the correct behaviour of this bit tbh, the rfcs don't seem to be specifc
// around whether quoted strings need to quote the quotes or not!! (that I can find anyway :) )
var value= pair[2].replace(/^"|"$/g, '')
value= value.replace(/\\"/g, '"')

results[pair[1]]= value
}

return results;
}
});
5 changes: 2 additions & 3 deletions lib/express/plugins/auth.strategies/http/http.js
Expand Up @@ -65,14 +65,13 @@ exports.Http= BaseHttpStrategy.extend({
},

_badRequest: function ( request, callback ) {
request.halt(400, 'Bad Request');
request.respond(400, 'Bad Request');
this.halt(callback)
},

_unAuthenticated: function( request, callback ) {
require('sys').puts('_unAuthenticated')
request.header('WWW-Authenticate', this.getAuthenticateResponseHeader());
request.halt(401, "Authorization Required");
request.respond(401, "Authorization Required");
this.halt(callback)
},

Expand Down
1 change: 0 additions & 1 deletion lib/express/plugins/auth.strategies/twitter.js
Expand Up @@ -33,7 +33,6 @@ exports.Twitter= AuthStrategy.extend({
username: additionalParameters.screen_name }

self.executionResult.user= user;

// Need to sort out redirection proeprly
//TODO: sort out the redirect to original url (currently tis a mess )
request.redirect("/twitter")
Expand Down
3 changes: 2 additions & 1 deletion spec/node.js
Expand Up @@ -23,7 +23,8 @@ specs = {
'strategies',
'auth.strategies.anonymous',
'auth.strategies.never',
'auth.strategy.base'
'auth.strategy.base',
'auth.strategies.http.digest'
]
}

Expand Down

0 comments on commit 15a8df6

Please sign in to comment.