strategy.js
14.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
// Load modules.
var passport = require('passport-strategy')
, url = require('url')
, util = require('util')
, utils = require('./utils')
, OAuth = require('oauth').OAuth
, SessionRequestTokenStore = require('./requesttoken/session')
, 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';
this._requestTokenStore = options.requestTokenStore || new SessionRequestTokenStore({ 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;
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' }
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 == 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;