lib/strategy.js
// Load modules.
var passport = require('passport-strategy')
, url = require('url')
, util = require('util')
, utils = require('./utils')
, OAuth = require('oauth').OAuth
, TokenStore = require('./requesttoken/session')
, StateStore = require('./requesttoken/state')
, InternalOAuthError = require('./errors/internaloautherror');
/**
* Creates an instance of `OAuthStrategy`.
*
* The OAuth authentication strategy authenticates requests using the OAuth
* protocol.
*
* OAuth provides a facility for delegated authentication, whereby users can
* authenticate using a third-party service such as Twitter. Delegating in this
* manner involves a sequence of events, including redirecting the user to the
* third-party service for authorization. Once authorization has been obtained,
* the user is redirected back to the application and a token can be used to
* obtain credentials.
*
* Applications must supply a `verify` callback, for which the function
* signature is:
*
* function(token, tokenSecret, profile, cb) { ... }
*
* The verify callback is responsible for finding or creating the user, and
* invoking `cb` with the following arguments:
*
* done(err, user, info);
*
* `user` should be set to `false` to indicate an authentication failure.
* Additional `info` can optionally be passed as a third argument, typically
* used to display informational messages. If an exception occured, `err`
* should be set.
*
* Options:
*
* - `requestTokenURL` URL used to obtain an unauthorized request token
* - `accessTokenURL` URL used to exchange a user-authorized request token for an access token
* - `userAuthorizationURL` URL used to obtain user authorization
* - `consumerKey` identifies client to service provider
* - `consumerSecret` secret used to establish ownership of the consumer key
* - 'signatureMethod' signature method used to sign the request (default: 'HMAC-SHA1')
* - `callbackURL` URL to which the service provider will redirect the user after obtaining authorization
* - `passReqToCallback` when `true`, `req` is the first argument to the verify callback (default: `false`)
*
* Examples:
*
* passport.use(new OAuthStrategy({
* requestTokenURL: 'https://www.example.com/oauth/request_token',
* accessTokenURL: 'https://www.example.com/oauth/access_token',
* userAuthorizationURL: 'https://www.example.com/oauth/authorize',
* consumerKey: '123-456-789',
* consumerSecret: 'shhh-its-a-secret'
* callbackURL: 'https://www.example.net/auth/example/callback'
* },
* function(token, tokenSecret, profile, cb) {
* User.findOrCreate(..., function (err, user) {
* cb(err, user);
* });
* }
* ));
*
* @constructor
* @param {Object} options
* @param {Function} verify
* @api public
*/
function OAuthStrategy(options, verify) {
if (typeof options == 'function') {
verify = options;
options = undefined;
}
options = options || {};
if (!verify) { throw new TypeError('OAuthStrategy requires a verify callback'); }
if (!options.requestTokenURL) { throw new TypeError('OAuthStrategy requires a requestTokenURL option'); }
if (!options.accessTokenURL) { throw new TypeError('OAuthStrategy requires a accessTokenURL option'); }
if (!options.userAuthorizationURL) { throw new TypeError('OAuthStrategy requires a userAuthorizationURL option'); }
if (!options.consumerKey) { throw new TypeError('OAuthStrategy requires a consumerKey option'); }
if (options.consumerSecret === undefined) { throw new TypeError('OAuthStrategy requires a consumerSecret option'); }
passport.Strategy.call(this);
this.name = 'oauth';
this._verify = verify;
// NOTE: The _oauth property is considered "protected". Subclasses are
// allowed to use it when making protected resource requests to retrieve
// the user profile.
this._oauth = new OAuth(options.requestTokenURL, options.accessTokenURL,
options.consumerKey, options.consumerSecret,
'1.0', null, options.signatureMethod || 'HMAC-SHA1',
null, options.customHeaders);
this._userAuthorizationURL = options.userAuthorizationURL;
this._callbackURL = options.callbackURL;
this._key = options.sessionKey || 'oauth';
var store = options.store || options.requestTokenStore;
if (store && typeof store == 'object') {
this._requestTokenStore = store;
} else if (store) {
this._requestTokenStore = new StateStore({ key: this._key })
} else {
this._requestTokenStore = new TokenStore({ key: this._key });
}
this._trustProxy = options.proxy;
this._passReqToCallback = options.passReqToCallback;
this._skipUserProfile = (options.skipUserProfile === undefined) ? false : options.skipUserProfile;
}
// Inherit from `passport.Strategy`.
util.inherits(OAuthStrategy, passport.Strategy);
/**
* Authenticate request by delegating to a service provider using OAuth.
*
* @param {Object} req
* @api protected
*/
OAuthStrategy.prototype.authenticate = function(req, options) {
options = options || {};
var self = this;
var meta = {
requestTokenURL: this._oauth._requestUrl,
accessTokenURL: this._oauth._accessUrl,
userAuthorizationURL: this._userAuthorizationURL,
consumerKey: this._oauth._consumerKey
}
if (req.query && req.query.oauth_token) {
// The request being authenticated contains an oauth_token parameter in the
// query portion of the URL. This indicates that the service provider has
// redirected the user back to the application, after authenticating the
// user and obtaining their authorization.
//
// The value of the oauth_token parameter is the request token. Together
// with knowledge of the token secret (stored in the session), the request
// token can be exchanged for an access token and token secret.
//
// This access token and token secret, along with the optional ability to
// fetch profile information from the service provider, is sufficient to
// establish the identity of the user.
var oauthToken = req.query.oauth_token;
function loaded(err, oauthTokenSecret, state) {
if (err) { return self.error(err); }
if (!oauthTokenSecret) {
return self.fail(state, 403);
}
// NOTE: The oauth_verifier parameter will be supplied in the query portion
// of the redirect URL, if the server supports OAuth 1.0a.
var oauthVerifier = req.query.oauth_verifier || null;
self._oauth.getOAuthAccessToken(oauthToken, oauthTokenSecret, oauthVerifier, function(err, token, tokenSecret, params) {
if (err) { return self.error(self._createOAuthError('Failed to obtain access token', err)); }
function destroyed(err) {
if (err) { return self.error(err); }
self._loadUserProfile(token, tokenSecret, params, function(err, profile) {
if (err) { return self.error(err); }
function verified(err, user, info) {
if (err) { return self.error(err); }
if (!user) { return self.fail(info); }
info = info || {};
if (state) { info.state = state; }
self.success(user, info);
}
try {
if (self._passReqToCallback) {
var arity = self._verify.length;
if (arity == 6) {
self._verify(req, token, tokenSecret, params, profile, verified);
} else { // arity == 5
self._verify(req, token, tokenSecret, profile, verified);
}
} else {
var arity = self._verify.length;
if (arity == 5) {
self._verify(token, tokenSecret, params, profile, verified);
} else { // arity == 4
self._verify(token, tokenSecret, profile, verified);
}
}
} catch (ex) {
return self.error(ex);
}
});
}
// The request token has been exchanged for an access token. Since the
// request token is a single-use token, that data can be removed from the
// store.
try {
var arity = self._requestTokenStore.destroy.length;
if (arity == 4) {
self._requestTokenStore.destroy(req, oauthToken, meta, destroyed);
} else { // arity == 3
self._requestTokenStore.destroy(req, oauthToken, destroyed);
}
} catch (ex) {
return self.error(ex);
}
});
}
try {
var arity = self._requestTokenStore.get.length;
if (arity == 4) {
this._requestTokenStore.get(req, oauthToken, meta, loaded);
} else { // arity == 3
this._requestTokenStore.get(req, oauthToken, loaded);
}
} catch (ex) {
return this.error(ex);
}
} else {
// In order to authenticate via OAuth, the application must obtain a request
// token from the service provider and redirect the user to the service
// provider to obtain their authorization. After authorization has been
// approved the user will be redirected back the application, at which point
// the application can exchange the request token for an access token.
//
// In order to successfully exchange the request token, its corresponding
// token secret needs to be known. The token secret will be temporarily
// stored in the session, so that it can be retrieved upon the user being
// redirected back to the application.
var params = this.requestTokenParams(options);
var callbackURL = options.callbackURL || this._callbackURL;
if (callbackURL) {
var parsed = url.parse(callbackURL);
if (!parsed.protocol) {
// The callback URL is relative, resolve a fully qualified URL from the
// URL of the originating request.
callbackURL = url.resolve(utils.originalURL(req, { proxy: this._trustProxy }), callbackURL);
}
}
params.oauth_callback = callbackURL;
meta.callbackURL = callbackURL;
this._oauth.getOAuthRequestToken(params, function(err, token, tokenSecret, params) {
if (err) { return self.error(self._createOAuthError('Failed to obtain request token', err)); }
// NOTE: params will contain an oauth_callback_confirmed property set to
// true, if the server supports OAuth 1.0a.
// { oauth_callback_confirmed: 'true' }
var state = options.state;
function stored(err) {
if (err) { return self.error(err); }
var parsed = url.parse(self._userAuthorizationURL, true);
parsed.query.oauth_token = token;
if (!params.oauth_callback_confirmed && callbackURL) {
// NOTE: If oauth_callback_confirmed=true is not present when issuing a
// request token, the server does not support OAuth 1.0a. In this
// circumstance, `oauth_callback` is passed when redirecting the
// user to the service provider.
parsed.query.oauth_callback = callbackURL;
}
utils.merge(parsed.query, self.userAuthorizationParams(options));
delete parsed.search;
var location = url.format(parsed);
self.redirect(location);
}
try {
var arity = self._requestTokenStore.set.length;
if (arity == 6) {
self._requestTokenStore.set(req, token, tokenSecret, state, meta, stored);
} else if (arity == 5) {
self._requestTokenStore.set(req, token, tokenSecret, meta, stored);
} else { // arity == 4
self._requestTokenStore.set(req, token, tokenSecret, stored);
}
} catch (ex) {
return self.error(ex);
}
});
}
};
/**
* Retrieve user profile from service provider.
*
* OAuth-based authentication strategies can overrride this function in order to
* load the user's profile from the service provider. This assists applications
* (and users of those applications) in the initial registration process by
* automatically submitting required information.
*
* @param {String} token
* @param {String} tokenSecret
* @param {Object} params
* @param {Function} done
* @api protected
*/
OAuthStrategy.prototype.userProfile = function(token, tokenSecret, params, done) {
return done(null, {});
};
/**
* Return extra parameters to be included in the request token request.
*
* Some OAuth providers require additional parameters to be included when
* issuing a request token. Since these parameters are not standardized by the
* OAuth specification, OAuth-based authentication strategies can overrride this
* function in order to populate these parameters as required by the provider.
*
* @param {Object} options
* @return {Object}
* @api protected
*/
OAuthStrategy.prototype.requestTokenParams = function(options) {
return {};
};
/**
* Return extra parameters to be included in the user authorization request.
*
* Some OAuth providers allow additional, non-standard parameters to be included
* when requesting authorization. Since these parameters are not standardized
* by the OAuth specification, OAuth-based authentication strategies can
* overrride this function in order to populate these parameters as required by
* the provider.
*
* @param {Object} options
* @return {Object}
* @api protected
*/
OAuthStrategy.prototype.userAuthorizationParams = function(options) {
return {};
};
/**
* Parse error response from OAuth endpoint.
*
* OAuth-based authentication strategies can overrride this function in order to
* parse error responses received from the request token and access token
* endpoints, allowing the most informative message to be displayed.
*
* If this function is not overridden, a generic error will be thrown.
*
* @param {String} body
* @param {Number} status
* @return {Error}
* @api protected
*/
OAuthStrategy.prototype.parseErrorResponse = function(body, status) {
return null;
};
/**
* Load user profile, contingent upon options.
*
* @param {String} accessToken
* @param {Function} done
* @api private
*/
OAuthStrategy.prototype._loadUserProfile = function(token, tokenSecret, params, done) {
var self = this;
function loadIt() {
return self.userProfile(token, tokenSecret, params, done);
}
function skipIt() {
return done(null);
}
if (typeof this._skipUserProfile == 'function' && this._skipUserProfile.length > 1) {
// async
this._skipUserProfile(token, tokenSecret, function(err, skip) {
if (err) { return done(err); }
if (!skip) { return loadIt(); }
return skipIt();
});
} else {
var skip = (typeof this._skipUserProfile == 'function') ? this._skipUserProfile() : this._skipUserProfile;
if (!skip) { return loadIt(); }
return skipIt();
}
};
/**
* Create an OAuth error.
*
* @param {String} message
* @param {Object|Error} err
* @api private
*/
OAuthStrategy.prototype._createOAuthError = function(message, err) {
var e;
if (err.statusCode && err.data) {
try {
e = this.parseErrorResponse(err.data, err.statusCode);
} catch (_) {}
}
if (!e) { e = new InternalOAuthError(message, err); }
return e;
};
// Expose constructor.
module.exports = OAuthStrategy;