Last active
July 3, 2018 06:20
-
-
Save eezhal92/cf6b3892231b4f3c1be4b7055d394283 to your computer and use it in GitHub Desktop.
passport-oauth2/strategy.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Load modules. | |
var passport = require('passport-strategy') | |
, url = require('url') | |
, util = require('util') | |
, utils = require('./utils') | |
, OAuth2 = require('oauth').OAuth2 | |
, NullStateStore = require('./state/null') | |
, SessionStateStore = require('./state/session') | |
, 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 | |
* - `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://www.example.com/oauth2/authorize', | |
* tokenURL: 'https://www.example.com/oauth2/token', | |
* clientID: '123-456-789', | |
* clientSecret: 'shhh-its-a-secret' | |
* callbackURL: 'https://www.example.net/auth/example/callback' | |
* }, | |
* 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.clientID) { throw new TypeError('OAuth2Strategy requires a clientID 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._callbackURL = options.callbackURL; | |
this._scope = options.scope; | |
this._scopeSeparator = options.scopeSeparator || ' '; | |
this._key = options.sessionKey || ('oauth2:' + url.parse(options.authorizationURL).hostname); | |
if (options.store) { | |
this._stateStore = options.store; | |
} else { | |
if (options.state) { | |
this._stateStore = new SessionStateStore({ key: this._key }); | |
} else { | |
this._stateStore = new NullStateStore(); | |
} | |
} | |
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) { | |
if (options.params) { | |
this._queryString = Object.keys(options.params).map(key => `${key}=${options.params[key]}`) | |
.join('&') | |
} | |
var qs = '?' + this._queryString || ''; | |
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; | |
callbackURL = callbackURL.includes('?') | |
? callbackURL | |
: callbackURL + qs | |
options.callbackURL = 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); | |
} | |
} | |
var meta = { | |
authorizationURL: this._oauth2._authorizeUrl, | |
tokenURL: this._oauth2._accessTokenUrl, | |
clientID: this._oauth2._clientId | |
} | |
if (req.query && req.query.code) { | |
function loaded(err, ok, state) { | |
if (err) { return self.error(err); } | |
if (!ok) { | |
return self.fail(state, 403); | |
} | |
var code = req.query.code; | |
var params = self.tokenParams(options); | |
params.grant_type = 'authorization_code'; | |
if (callbackURL) { params.redirect_uri = callbackURL; } | |
self._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); } | |
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, 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); | |
} | |
}); | |
} | |
); | |
} | |
var state = req.query.state; | |
try { | |
var arity = this._stateStore.verify.length; | |
if (arity == 4) { | |
this._stateStore.verify(req, state, meta, loaded); | |
} else { // arity == 3 | |
this._stateStore.verify(req, state, loaded); | |
} | |
} catch (ex) { | |
return this.error(ex); | |
} | |
} else { | |
var params = this.authorizationParams(options); | |
params.response_type = 'code'; | |
if (callbackURL) { params.redirect_uri = callbackURL } | |
var scope = options.scope || this._scope; | |
if (scope) { | |
if (Array.isArray(scope)) { scope = scope.join(this._scopeSeparator); } | |
params.scope = scope; | |
} | |
// console.log('<<<>>> AUTHORIZE WITH THIS', params) | |
var state = options.state; | |
if (state) { | |
params.state = state; | |
var parsed = url.parse(this._oauth2._authorizeUrl, true); | |
utils.merge(parsed.query, params); | |
parsed.query['client_id'] = this._oauth2._clientId; | |
delete parsed.search; | |
var location = url.format(parsed); | |
this.redirect(location); | |
} else { | |
function stored(err, state) { | |
if (err) { return self.error(err); } | |
if (state) { params.state = state; } | |
var parsed = url.parse(self._oauth2._authorizeUrl, true); | |
utils.merge(parsed.query, params); | |
parsed.query['client_id'] = self._oauth2._clientId; | |
delete parsed.search; | |
var location = url.format(parsed); | |
self.redirect(location); | |
} | |
try { | |
var arity = this._stateStore.store.length; | |
if (arity == 3) { | |
this._stateStore.store(req, meta, stored); | |
} else { // arity == 2 | |
this._stateStore.store(req, stored); | |
} | |
} catch (ex) { | |
return this.error(ex); | |
} | |
} | |
} | |
}; | |
/** | |
* 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(accessToken, done) { | |
return done(null, {}); | |
}; | |
/** | |
* 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. | |
* | |
* @param {String} accessToken | |
* @param {Function} done | |
* @api private | |
*/ | |
OAuth2Strategy.prototype._loadUserProfile = function(accessToken, done) { | |
var self = this; | |
function loadIt() { | |
return self.userProfile(accessToken, done); | |
} | |
function skipIt() { | |
return done(null); | |
} | |
if (typeof this._skipUserProfile == 'function' && this._skipUserProfile.length > 1) { | |
// async | |
this._skipUserProfile(accessToken, 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 | |
*/ | |
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 constructor. | |
module.exports = OAuth2Strategy; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment