-
-
Notifications
You must be signed in to change notification settings - Fork 445
/
strategy.js
242 lines (215 loc) · 7.71 KB
/
strategy.js
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
// Load modules.
var OAuth2Strategy = require('passport-oauth2')
, util = require('util')
, uri = require('url')
, crypto = require('crypto')
, Profile = require('./profile')
, InternalOAuthError = require('passport-oauth2').InternalOAuthError
, FacebookAuthorizationError = require('./errors/facebookauthorizationerror')
, FacebookTokenError = require('./errors/facebooktokenerror')
, FacebookGraphAPIError = require('./errors/facebookgraphapierror');
/**
* `Strategy` constructor.
*
* The Facebook authentication strategy authenticates requests by delegating to
* Facebook using the OAuth 2.0 protocol.
*
* Applications must supply a `verify` callback which accepts an `accessToken`,
* `refreshToken` and service-specific `profile`, and then calls the `cb`
* callback supplying a `user`, which should be set to `false` if the
* credentials are not valid. If an exception occured, `err` should be set.
*
* Options:
* - `clientID` your Facebook application's App ID
* - `clientSecret` your Facebook application's App Secret
* - `callbackURL` URL to which Facebook will redirect the user after granting authorization
*
* Examples:
*
* passport.use(new FacebookStrategy({
* clientID: '123-456-789',
* clientSecret: 'shhh-its-a-secret'
* callbackURL: 'https://www.example.net/auth/facebook/callback'
* },
* function(accessToken, refreshToken, profile, cb) {
* User.findOrCreate(..., function (err, user) {
* cb(err, user);
* });
* }
* ));
*
* @constructor
* @param {object} options
* @param {function} verify
* @access public
*/
function Strategy(options, verify) {
options = options || {};
var version = options.graphAPIVersion || 'v3.2';
options.authorizationURL = options.authorizationURL || 'https://www.facebook.com/' + version + '/dialog/oauth';
options.tokenURL = options.tokenURL || 'https://graph.facebook.com/' + version + '/oauth/access_token';
options.scopeSeparator = options.scopeSeparator || ',';
OAuth2Strategy.call(this, options, verify);
this.name = 'facebook';
this._profileURL = options.profileURL || 'https://graph.facebook.com/' + version + '/me';
this._profileFields = options.profileFields || null;
this._enableProof = options.enableProof;
this._clientSecret = options.clientSecret;
}
// Inherit from `OAuth2Strategy`.
util.inherits(Strategy, OAuth2Strategy);
/**
* Authenticate request by delegating to Facebook using OAuth 2.0.
*
* @param {http.IncomingMessage} req
* @param {object} options
* @access protected
*/
Strategy.prototype.authenticate = function(req, options) {
// Facebook doesn't conform to the OAuth 2.0 specification, with respect to
// redirecting with error codes.
//
// FIX: https://github.com/jaredhanson/passport-oauth/issues/16
if (req.query && req.query.error_code && !req.query.error) {
return this.error(new FacebookAuthorizationError(req.query.error_message, parseInt(req.query.error_code, 10)));
}
OAuth2Strategy.prototype.authenticate.call(this, req, options);
};
/**
* Return extra Facebook-specific parameters to be included in the authorization
* request.
*
* Options:
* - `display` Display mode to render dialog, { `page`, `popup`, `touch` }.
*
* @param {object} options
* @return {object}
* @access protected
*/
Strategy.prototype.authorizationParams = function (options) {
var params = {};
// https://developers.facebook.com/docs/reference/dialogs/oauth/
if (options.display) {
params.display = options.display;
}
// https://developers.facebook.com/docs/facebook-login/reauthentication/
if (options.authType) {
params.auth_type = options.authType;
}
if (options.authNonce) {
params.auth_nonce = options.authNonce;
}
return params;
};
/**
* Retrieve user profile from Facebook.
*
* This function constructs a normalized profile, with the following properties:
*
* - `provider` always set to `facebook`
* - `id` the user's Facebook ID
* - `username` the user's Facebook username
* - `displayName` the user's full name
* - `name.familyName` the user's last name
* - `name.givenName` the user's first name
* - `name.middleName` the user's middle name
* - `gender` the user's gender: `male` or `female`
* - `profileUrl` the URL of the profile for the user on Facebook
* - `emails` the proxied or contact email address granted by the user
*
* @param {string} accessToken
* @param {function} done
* @access protected
*/
Strategy.prototype.userProfile = function(accessToken, done) {
var url = uri.parse(this._profileURL);
if (this._enableProof) {
// Secure API call by adding proof of the app secret. This is required when
// the "Require AppSecret Proof for Server API calls" setting has been
// enabled. The proof is a SHA256 hash of the access token, using the app
// secret as the key.
//
// For further details, refer to:
// https://developers.facebook.com/docs/reference/api/securing-graph-api/
var proof = crypto.createHmac('sha256', this._clientSecret).update(accessToken).digest('hex');
url.search = (url.search ? url.search + '&' : '') + 'appsecret_proof=' + proof;
}
if (this._profileFields) {
var fields = this._convertProfileFields(this._profileFields);
if (fields !== '') { url.search = (url.search ? url.search + '&' : '') + 'fields=' + fields; }
}
url = uri.format(url);
this._oauth2.get(url, accessToken, function (err, body, res) {
var json;
if (err) {
if (err.data) {
try {
json = JSON.parse(err.data);
} catch (_) {}
}
if (json && json.error && typeof json.error == 'object') {
return done(new FacebookGraphAPIError(json.error.message, json.error.type, json.error.code, json.error.error_subcode, json.error.fbtrace_id));
}
return done(new InternalOAuthError('Failed to fetch user profile', err));
}
try {
json = JSON.parse(body);
} catch (ex) {
return done(new Error('Failed to parse user profile'));
}
var profile = Profile.parse(json);
profile.provider = 'facebook';
profile._raw = body;
profile._json = json;
done(null, profile);
});
};
/**
* Parse error response from Facebook OAuth 2.0 token endpoint.
*
* @param {string} body
* @param {number} status
* @return {Error}
* @access protected
*/
Strategy.prototype.parseErrorResponse = function(body, status) {
var json = JSON.parse(body);
if (json.error && typeof json.error == 'object') {
return new FacebookTokenError(json.error.message, json.error.type, json.error.code, json.error.error_subcode, json.error.fbtrace_id);
}
return OAuth2Strategy.prototype.parseErrorResponse.call(this, body, status);
};
/**
* Convert Facebook profile to a normalized profile.
*
* @param {object} profileFields
* @return {string}
* @access protected
*/
Strategy.prototype._convertProfileFields = function(profileFields) {
var map = {
'id': 'id',
'username': 'username',
'displayName': 'name',
'name': ['last_name', 'first_name', 'middle_name'],
'gender': 'gender',
'birthday': 'birthday',
'profileUrl': 'link',
'emails': 'email',
'photos': 'picture'
};
var fields = [];
profileFields.forEach(function(f) {
// return raw Facebook profile field to support the many fields that don't
// map cleanly to Portable Contacts
if (typeof map[f] === 'undefined') { return fields.push(f); };
if (Array.isArray(map[f])) {
Array.prototype.push.apply(fields, map[f]);
} else {
fields.push(map[f]);
}
});
return fields.join(',');
};
// Expose constructor.
module.exports = Strategy;