├── LICENSE ├── README.md └── parse-facebook-user-session.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Facebook, Inc. All rights reserved. 2 | 3 | You are hereby granted a non-exclusive, worldwide, royalty-free license to use, 4 | copy, modify, and distribute this software in source code or binary form for use 5 | in connection with the web services and APIs provided by Facebook. 6 | 7 | As with any software that integrates with the Facebook platform, your use of 8 | this software is subject to the Facebook Developer Principles and Policies 9 | [http://developers.facebook.com/policy/]. This copyright notice shall be 10 | included in all copies or substantial portions of the software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 14 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 16 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | ----- 20 | 21 | As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | parse-facebook-user-session 2 | =========================== 3 | 4 | A Cloud Code module to facilitate logging into an express website with Facebook. 5 | 6 | If you'd like to require users to be logged into Facebook to access pages 7 | on your site, it's easy using the `parseFacebookUserSession` 8 | middleware module. First set up the `parseExpressCookieSession` 9 | as described in 10 | our express docs. 11 | 12 | var parseExpressHttpsRedirect = require('parse-express-https-redirect'); 13 | var parseExpressCookieSession = require('parse-express-cookie-session'); 14 | var parseFacebookUserSession = require('cloud/parse-facebook-user-session'); 15 | 16 | // ... Configure the express app ... 17 | 18 | app.use(parseExpressHttpsRedirect()); // Require user to be on HTTPS. 19 | app.use(express.bodyParser()); 20 | app.use(express.cookieParser('YOUR_SIGNING_SECRET')); 21 | app.use(parseExpressCookieSession({ cookie: { maxAge: 3600000 } })); 22 | 23 | Then use the `parseFacebookUserSession` middleware on any page 24 | that you want to require Facebook login on. Whenever the page is visited, if 25 | the user has not logged into your app with Facebook, they will be redirected 26 | to Facebook's site to start an OAuth flow. Once they get a login token from 27 | Facebook, they will be redirected back to an endpoint on your site. In the 28 | example below, the endpoint is `/login`. The middleware module 29 | takes care of handling that endpoint for you, authenticating the user and 30 | redirecting them back to the original page they tried to visit. To enable this 31 | on every page, use the `app.use` express method.
32 | 33 | app.use(parseFacebookUserSession({ 34 | clientId: 'YOUR_FB_CLIENT_ID', 35 | appSecret: 'YOUR_FB_APP_SECRET', 36 | redirectUri: '/login', 37 | scope: 'user_friends', 38 | })); 39 | 40 | If you'd like to only require Facebook Login on certain pages, you can include 41 | the middleware in your routing commands. 42 | 43 | var fbLogin = parseFacebookUserSession({ 44 | clientId: 'YOUR_FB_CLIENT_ID', 45 | appSecret: 'YOUR_FB_APP_SECRET', 46 | redirectUri: '/login', 47 | scope: 'user_friends', 48 | }); 49 | 50 | // To handle the login redirect. 51 | app.get('/login', fbLogin, function(req, res) {}); 52 | 53 | app.get('/stuff', fbLogin, function(req, res) { 54 | // This page requires Facebook login. 55 | }); 56 | 57 | You can access the user on any page with `Parse.User.current`. 58 | 59 | app.get('/', function(req, res) { 60 | var user = Parse.User.current(); 61 | 62 | res.render('hello', { 63 | message: 'Congrats, you are logged in, ' + user.get('username') + '!' 64 | }); 65 | }); 66 | 67 | You should also provide an endpoint to let the user log out. This is as simple 68 | as calling `Parse.User.logOut()`. 69 | 70 | app.get('/logout', function(req, res) { 71 | Parse.User.logOut(); 72 | res.render('hello', { message: 'You are now logged out!' }); 73 | }); 74 | 75 | As a side effect of using this module, you will see objects created in your 76 | app's data for the `ParseFacebookTokenRequest` class. You can 77 | safely ignore this class. It is used to keep track of the CSRF protection 78 | tokens and redirect URLs of users who are currently in the process of 79 | logging in. 80 | 81 | In order for the `ParseFacebookTokenRequest` to be created, you may need to 82 | enabled client class creation in your app's settings, if you've previously 83 | disabled it. Once the `ParseFacebookTokenRequest` class is created, you should 84 | go into the [class-level permissions](http://blog.parse.com/2014/07/07/parse-security-ii-class-hysteria/) for the class and disable all permissions. This cloud module uses 85 | master key access to access the tables. 86 | 87 | ----- 88 | 89 | As of April 5, 2017, Parse, LLC has transferred this code to the parse-community organization, and will no longer be contributing to or distributing this code. 90 | -------------------------------------------------------------------------------- /parse-facebook-user-session.js: -------------------------------------------------------------------------------- 1 | 2 | var moment = require('moment'); 3 | var querystring = require('querystring'); 4 | 5 | /** 6 | * @name parseFacebookUserSession 7 | * @class 8 | * 9 | *A middleware module for logging in a Parse.User using Facebook in express. 10 | * For more information, see our 11 | * hosting guide.
12 | * 13 | *Params includes the following:
14 | * clientId (required): The client id of the Facebook App. 15 | * appSecret (required): The app secret of the Facebook App. 16 | * scope (optional): What type of access you need. A comma separated list of scopes. 17 | * verbose (optional): If true, output debug messages to console.log. 18 | * redirectUri (optional): The path on this server to use for handling the 19 | * redirect callback from Facebook. If this is omitted, it defaults to 20 | * /login.21 | */ 22 | var parseFacebookUserSession = function(params) { 23 | params = params || {}; 24 | 25 | if (!params.clientId || !params.appSecret) { 26 | throw "You must specify a Facebook clientId and appSecret."; 27 | } 28 | 29 | /** 30 | * Logs using console.log if verbose is passed in when configuring the 31 | * middleware. 32 | */ 33 | var maybeLog = function() { 34 | if (params.verbose) { 35 | console.log.apply(console, arguments); 36 | } 37 | }; 38 | 39 | var relativeRedirectUri = params.redirectUri || "/login"; 40 | 41 | /** 42 | * Returns the absolute url of the redirect path for this request. 43 | */ 44 | var getAbsoluteRedirectUri = function(req) { 45 | return req.protocol + "://" + req.headers.host + req.app.path() + relativeRedirectUri; 46 | }; 47 | 48 | /** 49 | * Starts the Facebook login OAuth process. 50 | */ 51 | var beginLogin = function(req, res) { 52 | maybeLog("Starting Facebook login..."); 53 | 54 | Parse.Promise.as().then(function() { 55 | // Make a request object. Its objectId will be our XSRF token. 56 | maybeLog("Creating ParseFacebookTokenRequest..."); 57 | var request = new Parse.Object("ParseFacebookTokenRequest"); 58 | return request.save({ 59 | url: req.url.slice(1), 60 | ACL: new Parse.ACL() 61 | }); 62 | 63 | }).then(function(request) { 64 | maybeLog("Redirecting for Facebook OAuth."); 65 | 66 | // Put the XSRF token into a cookie so that we can match it later. 67 | res.cookie("requestId", request.id); 68 | 69 | // Redirect the user to start the Facebook OAuth flow. 70 | var url = 'https://www.facebook.com/dialog/oauth?'; 71 | url = url + querystring.stringify({ 72 | client_id: params.clientId, 73 | redirect_uri: getAbsoluteRedirectUri(req), 74 | state: request.id, 75 | scope: params.scope 76 | }); 77 | res.redirect(302, url); 78 | 79 | }); 80 | }; 81 | 82 | /** 83 | * Handles the last stage of the Facebook login OAuth redirect. 84 | */ 85 | var endLogin = function(req, res) { 86 | maybeLog("Handling request callback for Facebook login..."); 87 | 88 | if (req.query.state !== req.cookies.requestId) { 89 | maybeLog("Request failed XSRF validation."); 90 | res.send(500, "Bad Request"); 91 | return; 92 | } 93 | 94 | var url = 'https://graph.facebook.com/oauth/access_token?'; 95 | url = url + querystring.stringify({ 96 | client_id: params.clientId, 97 | redirect_uri: getAbsoluteRedirectUri(req), 98 | client_secret: params.appSecret, 99 | code: req.query.code 100 | }); 101 | 102 | var accessToken = null; 103 | var expires = null; 104 | var facebookData = null; 105 | 106 | Parse.Promise.as().then(function() { 107 | maybeLog("Fetching access token..."); 108 | return Parse.Cloud.httpRequest({ url: url }); 109 | 110 | }).then(function(response) { 111 | maybeLog("Fetching user data from Facebook..."); 112 | 113 | var data = querystring.parse(response.text); 114 | accessToken = data.access_token; 115 | expires = data.expires; 116 | 117 | var url = 'https://graph.facebook.com/me?'; 118 | url = url + querystring.stringify({ 119 | access_token: accessToken 120 | }); 121 | return Parse.Cloud.httpRequest({ url: url }); 122 | 123 | }).then(function(response) { 124 | maybeLog("Logging into Parse with Facebook token..."); 125 | 126 | facebookData = response.data; 127 | var expiration = moment().add('seconds', expires).format( 128 | "YYYY-MM-DDTHH:mm:ss.SSS\\Z"); 129 | 130 | return Parse.FacebookUtils.logIn({ 131 | id: response.data.id, 132 | access_token: accessToken, 133 | expiration_date: expiration 134 | }); 135 | 136 | }).then(function(response) { 137 | maybeLog("Becoming Parse user..."); 138 | return Parse.User.become(response.getSessionToken()); 139 | 140 | }).then(function(user) { 141 | maybeLog("Saving Facebook data for user..."); 142 | user.set("name", facebookData.name); 143 | return user.save(); 144 | 145 | }).then(function(user) { 146 | maybeLog("Fetching ParseFacebookTokenRequest for " + 147 | req.query.state + "..."); 148 | var request = new Parse.Object("ParseFacebookTokenRequest"); 149 | request.id = req.query.state; 150 | return request.fetch({ useMasterKey: true }); 151 | 152 | }).then(function(request) { 153 | maybeLog("Deleting used ParseFacebookTokenRequest..."); 154 | // Let's delete this request so that no one can reuse the token. 155 | var url = request.get("url"); 156 | return request.destroy({ useMasterKey: true }).then(function() { 157 | return url; 158 | }); 159 | 160 | }).then(function(url) { 161 | maybeLog("Success!"); 162 | res.redirect(302, url); 163 | }, function(error) { 164 | maybeLog("Failed! " + JSON.stringify(error)); 165 | res.send(500, error); 166 | }); 167 | }; 168 | 169 | /** 170 | * The actual middleware method. 171 | */ 172 | return function(req, res, next) { 173 | // If the user is already logged in, there's nothing to do. 174 | if (Parse.User.current()) { 175 | return next(); 176 | } 177 | 178 | // If this is the Facebook login redirect URL, and a code is present, then handle the code. 179 | // If a code is not present, begin the login process. 180 | if (req.path === relativeRedirectUri && req.query.code) { 181 | endLogin(req, res); 182 | } else { 183 | beginLogin(req, res); 184 | } 185 | }; 186 | }; 187 | 188 | module.exports = parseFacebookUserSession; 189 | --------------------------------------------------------------------------------