passport-gigya-oauth2
Version:
Gigya OAuth 2.0 authentication strategy for Passport.
446 lines (405 loc) • 14.9 kB
JavaScript
/**
* Module dependencies.
*/
var passport = require('passport-strategy'),
https = require('https'),
url = require('url'),
uid = require('uid2'),
util = require('util'),
utils = require('./utils'),
OAuth2 = require('oauth').OAuth2,
AuthorizationError = require('./errors/authorizationerror'),
TokenError = require('./errors/tokenerror'),
InternalOAuthError = require('./errors/internaloautherror');
/**
* Creates an instance of `OAuth2Strategy`.
*
* The OAuth 2.0 authentication strategy authenticates requests using the OAuth
* 2.0 framework.
*
* OAuth 2.0 provides a facility for delegated authentication, whereby users can
* authenticate using a third-party service such as Facebook. Delegating in
* this manner involves a sequence of events, including redirecting the user to
* the third-party service for authorization. Once authorization has been
* granted, the user is redirected back to the application and an authorization
* code can be used to obtain credentials.
*
* Applications must supply a `verify` callback, for which the function
* signature is:
*
* function(accessToken, refreshToken, profile, done) { ... }
*
* The verify callback is responsible for finding or creating the user, and
* invoking `done` 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:
*
* - `authorizationURL` URL used to obtain an authorization grant
* - `tokenURL` URL used to obtain an access token
* - `userInfoURL` URL used to obtain user information
* - `clientID` identifies client to service provider
* - `clientSecret` secret used to establish ownership of the client identifer
* - `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 OAuth2Strategy({
* authorizationURL: 'https://<Data_Center>/oidc/op/v1.0/<API_KEY>/authorize',
* tokenURL: 'https://<Data_Center>/oidc/op/v1.0/<API_KEY>/token',
* userInfoURL: 'https://<Data_Center>/oidc/op/v1.0/<API_KEY>/userinfo',
* clientID: '123-456-789',
* clientSecret: 'shhh-its-a-secret'
* callbackURL: 'https://www.example.net/auth/example/callback',
* customHeaders: {'Authorization': 'Basic MTIzLTQ1Ni03ODk6c2hoaC1pdHMtYS1zZWNyZXQ='}
* },
* function(accessToken, refreshToken, profile, done) {
* User.findOrCreate(..., function (err, user) {
* done(err, user);
* });
* }
* ));
*
* @constructor
* @param {Object} options
* @param {Function} verify
* @api public
*/
function OAuth2Strategy(options, verify) {
if (typeof options == 'function') {
verify = options;
options = undefined;
}
options = options || {};
if (!verify) {
throw new TypeError('OAuth2Strategy requires a verify callback');
}
if (!options.authorizationURL) {
throw new TypeError('OAuth2Strategy requires a authorizationURL option');
}
if (!options.tokenURL) {
throw new TypeError('OAuth2Strategy requires a tokenURL option');
}
if (!options.userInfoURL) {
throw new TypeError('OAuth2Strategy requires a userInfoURL option');
}
if (!options.clientID) {
throw new TypeError('OAuth2Strategy requires a clientID option');
}
if (!options.clientSecret) {
throw new TypeError('OAuth2Strategy requires a clientSecret option');
}
passport.Strategy.call(this);
this.name = 'oauth2';
this._verify = verify;
// NOTE: The _oauth2 property is considered "protected". Subclasses are
// allowed to use it when making protected resource requests to retrieve
// the user profile.
this._oauth2 = new OAuth2(options.clientID, options.clientSecret,
'', options.authorizationURL, options.tokenURL, options.customHeaders);
this._userInfoURL = options.userInfoURL;
this._callbackURL = options.callbackURL;
this._scope = options.scope;
this._scopeSeparator = options.scopeSeparator || ' ';
this._state = options.state;
this._key = options.sessionKey || ('oauth2:' + url.parse(options.authorizationURL).hostname);
this._trustProxy = options.proxy;
this._passReqToCallback = options.passReqToCallback;
this._skipUserProfile = (options.skipUserProfile === undefined) ? false : options.skipUserProfile;
}
/**
* Inherit from `passport.Strategy`.
*/
util.inherits(OAuth2Strategy, passport.Strategy);
/**
* Authenticate request by delegating to a service provider using OAuth 2.0.
*
* @param {Object} req
* @api protected
*/
OAuth2Strategy.prototype.authenticate = function(req, options) {
options = options || {};
var self = this;
if (req.query && req.query.error) {
if (req.query.error == 'access_denied') {
return this.fail({
message: req.query.error_description
});
} else {
return this.error(new AuthorizationError(req.query.error_description, req.query.error, req.query.error_uri));
}
}
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);
}
}
if (req.query && req.query.code) {
var code = req.query.code;
if (this._state) {
if (!req.session) {
return this.error(new Error('OAuth2Strategy requires session support when using state. Did you forget app.use(express.session(...))?'));
}
var key = this._key;
if (!req.session[key]) {
return this.fail({
message: 'Unable to verify authorization request state.'
}, 403);
}
var state = req.session[key].state;
if (!state) {
return this.fail({
message: 'Unable to verify authorization request state.'
}, 403);
}
delete req.session[key].state;
if (Object.keys(req.session[key]).length === 0) {
delete req.session[key];
}
if (state !== req.query.state) {
return this.fail({
message: 'Invalid authorization request state.'
}, 403);
}
}
var params = this.tokenParams(options);
params.grant_type = 'authorization_code';
params.redirect_uri = callbackURL;
this._oauth2.getOAuthAccessToken(code, params,
function(err, accessToken, refreshToken, params) {
if (err) {
return self.error(self._createOAuthError('Failed to obtain access token', err));
}
self._loadUserProfile(accessToken, 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);
}
self.success(user, info);
}
try {
if (self._passReqToCallback) {
var arity = self._verify.length;
if (arity == 6) {
self._verify(req, accessToken, refreshToken, params, profile, verified);
} else { // arity == 5
self._verify(req, accessToken, refreshToken, profile, verified);
}
} else {
var arity = self._verify.length;
if (arity == 5) {
self._verify(accessToken, refreshToken, params, profile, verified);
} else { // arity == 4
self._verify(accessToken, refreshToken, profile, verified);
}
}
} catch (ex) {
return self.error(ex);
}
});
}
);
} else {
var params = this.authorizationParams(options);
params.response_type = 'code';
params.redirect_uri = callbackURL;
var scope = options.scope || this._scope;
if (scope) {
if (Array.isArray(scope)) {
scope = scope.join(this._scopeSeparator);
}
params.scope = scope;
}
var state = options.state;
if (state) {
params.state = state;
} else if (this._state) {
if (!req.session) {
return this.error(new Error('OAuth2Strategy requires session support when using state. Did you forget app.use(express.session(...))?'));
}
var key = this._key;
state = uid(24);
if (!req.session[key]) {
req.session[key] = {};
}
req.session[key].state = state;
params.state = state;
}
var location = this._oauth2.getAuthorizeUrl(params);
this.redirect(location);
}
};
/**
* Retrieve user profile from service provider.
*
* OAuth 2.0-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} accessToken
* @param {Function} done
* @api protected
*/
OAuth2Strategy.prototype.userProfile = function(id_token, done) {
return done(null, id_token);
};
/**
* Return extra parameters to be included in the authorization request.
*
* Some OAuth 2.0 providers allow additional, non-standard parameters to be
* included when requesting authorization. Since these parameters are not
* standardized by the OAuth 2.0 specification, OAuth 2.0-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
*/
OAuth2Strategy.prototype.authorizationParams = function(options) {
return {};
};
/**
* Return extra parameters to be included in the token request.
*
* Some OAuth 2.0 providers allow additional, non-standard parameters to be
* included when requesting an access token. Since these parameters are not
* standardized by the OAuth 2.0 specification, OAuth 2.0-based authentication
* strategies can overrride this function in order to populate these parameters
* as required by the provider.
*
* @return {Object}
* @api protected
*/
OAuth2Strategy.prototype.tokenParams = function(options) {
return {};
};
/**
* Parse error response from OAuth 2.0 endpoint.
*
* OAuth 2.0-based authentication strategies can overrride this function in
* order to parse error responses received from the token endpoint, allowing the
* most informative message to be displayed.
*
* If this function is not overridden, the body will be parsed in accordance
* with RFC 6749, section 5.2.
*
* @param {String} body
* @param {Number} status
* @return {Error}
* @api protected
*/
OAuth2Strategy.prototype.parseErrorResponse = function(body, status) {
var json = JSON.parse(body);
if (json.error) {
return new TokenError(json.error_description, json.error, json.error_uri);
}
return null;
};
/**
* Load user profile, contingent upon options.
* Customized for Gigya /userinfo endpoint.
*
* @param {String} access_token
* @param {Function} done
* @api private
*/
OAuth2Strategy.prototype._loadUserProfile = function(access_token, done) {
var self = this;
var urlNeedsParsing = this._userInfoURL;
var parsedUrl = url.parse(urlNeedsParsing, true);
if (parsedUrl.protocol == "https:" && !parsedUrl.port) {
parsedUrl.port = 443;
}
var options = {
host: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
method: 'GET',
headers: {
'Authorization': 'Bearer ' + access_token
}
};
var req = https.request(options, function(res) {
var output = '';
res.on('data', function(chunk) {
output += chunk;
});
res.on('end', function() {
var jsonData = JSON.parse(output);
function loadIt() {
return self.userProfile(jsonData, done);
}
function skipIt() {
return done(null);
}
if (typeof this._skipUserProfile == 'function' && this._skipUserProfile.length > 1) {
// async
this._skipUserProfile(jsonData, 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();
}
});
});
req.on('error', function(err) {
if (err) {
return done(err);
}
});
req.end();
};
/**
* Create an OAuth error.
*
* @param {String} message
* @param {Object|Error} err
* @api private
*/
OAuth2Strategy.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 `OAuth2Strategy`.
*/
module.exports = OAuth2Strategy;