// 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;