passport-indieauth
Version:
IndieAuth authentication strategy for Passport.
158 lines (146 loc) • 4.79 kB
JavaScript
/**
* Module dependencies.
*/
var mf = require('microformat-node');
var passport = require('passport-strategy');
var requester = require('request');
var querystring = require('querystring');
var util = require('util');
var _ = require('underscore');
/**
* `Strategy` constructor.
*
* The IndieAuth authentication strategy authenticates requests by delegating to
* an IndieAuth server using the IndieAuth protocol, which handles third-party
* authentication validation itself.
*
* Options:
* `service`: the URI for the IndieAuth service (by default `https://indieauth.com/auth`)
* `client_id`: your client ID (i.e. your application's identity, typically your domain)
* `redirect_uri`: your website's callback URL.
*
* Examples:
*
* passport.use(new IndieAuthStrategy({
* service: "https://indieauth.com/auth",
* client_id: "https://yourdomain.com",
* redirect_uri: "http://yourdomain.com/auth/indieauth/callback"
* },
* function(profile, done) {
* User.findOrCreate(..., function (err, user) {
* done(err, user);
* });
* }
* ));
*
* @param {Object} options
* @param {Function} verify
* @api public
*/
function Strategy(options, verify) {
options = options || {};
options.service = options.service || 'https://indieauth.com/auth';
passport.Strategy.call(this);
this.name = 'indieauth';
this._verify = verify;
this._service = options.service;
this._clientID = options.clientID;
this._callbackURL = options.callbackURL;
this._passReqToCallback = options.passReqToCallback;
}
/**
* Inherit from `passport.Strategy`.
*/
util.inherits(Strategy, passport.Strategy);
Strategy.prototype.authenticate = function(req, options, verify) {
var self = this;
if(req.query && req.query.code) {
var code = req.query.code;
requester.post({
uri: self._service,
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
body: 'code=' + code + '&client_id=' + self._clientID + '&redirect_uri=' + self._callbackURL
}, function(err, response, body) {
if(response.statusCode!=200) {
var errDesc = querystring.parse(body);
errDesc = errDesc.error_description;
return self.error(errDesc);
} else {
var domain = querystring.parse(body);
domain = domain.me;
self.userProfile(domain, function(err, profile) {
function verified(err, user, info) {
if (err) { return self.error(err); }
if (!user) { return self.fail(info); }
self.success(user, info);
}
try {
if (self._passReqToCallback) {
var arity = self._verify.length;
if (arity == 4) {
self._verify(req, params, profile, verified);
} else { // arity == 3
self._verify(req, profile, verified);
}
} else {
var arity = self._verify.length;
if (arity == 3) {
self._verify(params, profile, verified);
} else { // arity == 2
self._verify(profile, verified);
}
}
} catch (ex) {
return self.error(ex);
}
});
}
});
}
}
/**
* Retrieve user profile from user's verified domain.
*
* This function constructs a normalized(ish) profile, with the following properties:
*
* - `provider` always set to `indieauth`
* - `id` always set to the user's domain
* - Whatever available h-card properties are appended to the user profile.
* - No Portable Contacts Unifed Profile Schema
*
* @param {String} domain
* @param {Function} done
* @api protected
*/
Strategy.prototype.userProfile = function(domain, done) {
mf.parseUrl(domain, { 'filters': ['h-card'] }, function(err, data) {
if(err) {
done(err, null);
} else {
var profile = {
provider: 'indieauth',
id: domain,
};
var hcard;
// Matches the user's domain name with the URL value of their h-card.
hcard = _.findWhere(data.items, { url: [domain] });
// If it does not match, we will assume it is just the first h-card on the page.
// Yes, this could result in false positives, but we have no other good way
// to correlate h-cards with the author's domain otherwise.
// Note: My anecdotal research shows this to be a pretty reliable assumption.
if(!hcard) {
hcard = data.items[0].properties;
} else {
hcard = { url: domain };
}
profile = _.extend(profile, hcard);
done(null, profile);
}
});
}
/**
* Expose `Strategy`.
*/
module.exports = Strategy;