1+ /**
2+ * Login With GitHub
3+ *
4+ * An example web application implementing OAuth2 in Cloud Code
5+ *
6+ * There will be four routes:
7+ * / - The main route will show a page with a Login with GitHub link
8+ * JavaScript will detect if it's logged in and navigate to /main
9+ * /authorize - This url will start the OAuth process and redirect to GitHub
10+ * /oauthCallback - Sent back from GitHub, this will validate the authorization
11+ * and create/update a Parse User before using 'become' to
12+ * set the user on the client side and redirecting to /main
13+ * /main - The application queries and displays some of the users GitHub data
14+ *
15+ * @author Fosco Marotto (Facebook) <[email protected] > 16+ */
17+
18+ /**
19+ * Load needed modules.
20+ */
21+ var express = require ( 'express' ) ;
22+ var querystring = require ( 'querystring' ) ;
23+ var _ = require ( 'underscore' ) ;
24+ var Buffer = require ( 'buffer' ) . Buffer ;
25+
26+ /**
27+ * Create an express application instance
28+ */
29+ var app = express ( ) ;
30+
31+ /**
32+ * GitHub specific details, including application id and secret
33+ */
34+ var githubClientId = 'your-github-client-id-here' ;
35+ var githubClientSecret = 'your-github-client-secret-here' ;
36+
37+ var githubRedirectEndpoint = 'https://github.com/login/oauth/authorize?' ;
38+ var githubValidateEndpoint = 'https://github.com/login/oauth/access_token' ;
39+ var githubUserEndpoint = 'https://api.github.com/user' ;
40+
41+ /**
42+ * In the Data Browser, set the Class Permissions for these 2 classes to
43+ * disallow public access for Get/Find/Create/Update/Delete operations.
44+ * Only the master key should be able to query or write to these classes.
45+ */
46+ var TokenRequest = Parse . Object . extend ( "TokenRequest" ) ;
47+ var TokenStorage = Parse . Object . extend ( "TokenStorage" ) ;
48+
49+ /**
50+ * Create a Parse ACL which prohibits public access. This will be used
51+ * in several places throughout the application, to explicitly protect
52+ * Parse User, TokenRequest, and TokenStorage objects.
53+ */
54+ var restrictedAcl = new Parse . ACL ( ) ;
55+ restrictedAcl . setPublicReadAccess ( false ) ;
56+ restrictedAcl . setPublicWriteAccess ( false ) ;
57+
58+ /**
59+ * Global app configuration section
60+ */
61+ app . set ( 'views' , 'cloud/views' ) ; // Specify the folder to find templates
62+ app . set ( 'view engine' , 'ejs' ) ; // Set the template engine
63+ app . use ( express . bodyParser ( ) ) ; // Middleware for reading request body
64+
65+ /**
66+ * Main route.
67+ *
68+ * When called, render the login.ejs view
69+ */
70+ app . get ( '/' , function ( req , res ) {
71+ res . render ( 'login' , { } ) ;
72+ } ) ;
73+
74+ /**
75+ * Login with GitHub route.
76+ *
77+ * When called, generate a request token and redirect the browser to GitHub.
78+ */
79+ app . get ( '/authorize' , function ( req , res ) {
80+
81+ var tokenRequest = new TokenRequest ( ) ;
82+ // Secure the object against public access.
83+ tokenRequest . setACL ( restrictedAcl ) ;
84+ /**
85+ * Save this request in a Parse Object for validation when GitHub responds
86+ * Use the master key because this class is protected
87+ */
88+ tokenRequest . save ( null , { useMasterKey : true } ) . then ( function ( obj ) {
89+ /**
90+ * Redirect the browser to GitHub for authorization.
91+ * This uses the objectId of the new TokenRequest as the 'state'
92+ * variable in the GitHub redirect.
93+ */
94+ res . redirect (
95+ githubRedirectEndpoint + querystring . stringify ( {
96+ client_id : githubClientId ,
97+ state : obj . id
98+ } )
99+ ) ;
100+ } , function ( error ) {
101+ // If there's an error storing the request, render the error page.
102+ res . render ( 'error' , { errorMessage : 'Failed to save auth request.' } ) ;
103+ } ) ;
104+
105+ } ) ;
106+
107+ /**
108+ * OAuth Callback route.
109+ *
110+ * This is intended to be accessed via redirect from GitHub. The request
111+ * will be validated against a previously stored TokenRequest and against
112+ * another GitHub endpoint, and if valid, a User will be created and/or
113+ * updated with details from GitHub. A page will be rendered which will
114+ * 'become' the user on the client-side and redirect to the /main page.
115+ */
116+ app . get ( '/oauthCallback' , function ( req , res ) {
117+ var data = req . query ;
118+ var token ;
119+ /**
120+ * Validate that code and state have been passed in as query parameters.
121+ * Render an error page if this is invalid.
122+ */
123+ if ( ! ( data && data . code && data . state ) ) {
124+ res . render ( 'error' , { errorMessage : 'Invalid auth response received.' } ) ;
125+ return ;
126+ }
127+ var query = new Parse . Query ( TokenRequest ) ;
128+ /**
129+ * Check if the provided state object exists as a TokenRequest
130+ * Use the master key as operations on TokenRequest are protected
131+ */
132+ Parse . Cloud . useMasterKey ( ) ;
133+ Parse . Promise . as ( ) . then ( function ( ) {
134+ return query . get ( data . state ) ;
135+ } ) . then ( function ( obj ) {
136+ // Destroy the TokenRequest before continuing.
137+ return obj . destroy ( ) ;
138+ } ) . then ( function ( ) {
139+ // Validate & Exchange the code parameter for an access token from GitHub
140+ return getGitHubAccessToken ( data . code ) ;
141+ } ) . then ( function ( access ) {
142+ /**
143+ * Process the response from GitHub, return either the getGitHubUserDetails
144+ * promise, or reject the promise.
145+ */
146+ var githubData = access . data ;
147+ if ( githubData && githubData . access_token && githubData . token_type ) {
148+ token = githubData . access_token ;
149+ return getGitHubUserDetails ( token ) ;
150+ } else {
151+ return Parse . Promise . error ( "Invalid access request." ) ;
152+ }
153+ } ) . then ( function ( userDataResponse ) {
154+ /**
155+ * Process the users GitHub details, return either the upsertGitHubUser
156+ * promise, or reject the promise.
157+ */
158+ var userData = userDataResponse . data ;
159+ if ( userData && userData . login && userData . id ) {
160+ return upsertGitHubUser ( token , userData ) ;
161+ } else {
162+ return Parse . Promise . error ( "Unable to parse GitHub data" ) ;
163+ }
164+ } ) . then ( function ( user ) {
165+ /**
166+ * Render a page which sets the current user on the client-side and then
167+ * redirects to /main
168+ */
169+ res . render ( 'store_auth' , { sessionToken : user . getSessionToken ( ) } ) ;
170+ } , function ( error ) {
171+ /**
172+ * If the error is an object error (e.g. from a Parse function) convert it
173+ * to a string for display to the user.
174+ */
175+ if ( error && error . code && error . error ) {
176+ error = error . code + ' ' + error . error ;
177+ }
178+ res . render ( 'error' , { errorMessage : JSON . stringify ( error ) } ) ;
179+ } ) ;
180+
181+ } ) ;
182+
183+ /**
184+ * Logged in route.
185+ *
186+ * JavaScript will validate login and call a Cloud function to get the users
187+ * GitHub details using the stored access token.
188+ */
189+ app . get ( '/main' , function ( req , res ) {
190+ res . render ( 'main' , { } ) ;
191+ } ) ;
192+
193+ /**
194+ * Attach the express app to Cloud Code to process the inbound request.
195+ */
196+ app . listen ( ) ;
197+
198+ /**
199+ * Cloud function which will load a user's accessToken from TokenStorage and
200+ * request their details from GitHub for display on the client side.
201+ */
202+ Parse . Cloud . define ( 'getGitHubData' , function ( request , response ) {
203+ if ( ! request . user ) {
204+ return response . error ( 'Must be logged in.' ) ;
205+ }
206+ var query = new Parse . Query ( TokenStorage ) ;
207+ query . equalTo ( 'user' , request . user ) ;
208+ query . ascending ( 'createdAt' ) ;
209+ Parse . Promise . as ( ) . then ( function ( ) {
210+ return query . first ( { useMasterKey : true } ) ;
211+ } ) . then ( function ( tokenData ) {
212+ if ( ! tokenData ) {
213+ return Parse . Promise . error ( 'No GitHub data found.' ) ;
214+ }
215+ return getGitHubUserDetails ( tokenData . get ( 'accessToken' ) ) ;
216+ } ) . then ( function ( userDataResponse ) {
217+ var userData = userDataResponse . data ;
218+ response . success ( userData ) ;
219+ } , function ( error ) {
220+ response . error ( error ) ;
221+ } ) ;
222+ } ) ;
223+
224+ /**
225+ * This function is called when GitHub redirects the user back after
226+ * authorization. It calls back to GitHub to validate and exchange the code
227+ * for an access token.
228+ */
229+ var getGitHubAccessToken = function ( code ) {
230+ var body = querystring . stringify ( {
231+ client_id : githubClientId ,
232+ client_secret : githubClientSecret ,
233+ code : code
234+ } ) ;
235+ return Parse . Cloud . httpRequest ( {
236+ method : 'POST' ,
237+ url : githubValidateEndpoint ,
238+ headers : {
239+ 'Accept' : 'application/json' ,
240+ 'User-Agent' : 'Parse.com Cloud Code'
241+ } ,
242+ body : body
243+ } ) ;
244+ }
245+
246+ /**
247+ * This function calls the githubUserEndpoint to get the user details for the
248+ * provided access token, returning the promise from the httpRequest.
249+ */
250+ var getGitHubUserDetails = function ( accessToken ) {
251+ return Parse . Cloud . httpRequest ( {
252+ method : 'GET' ,
253+ url : githubUserEndpoint ,
254+ params : { access_token : accessToken } ,
255+ headers : {
256+ 'User-Agent' : 'Parse.com Cloud Code'
257+ }
258+ } ) ;
259+ }
260+
261+ /**
262+ * This function checks to see if this GitHub user has logged in before.
263+ * If the user is found, update the accessToken (if necessary) and return
264+ * the users session token. If not found, return the newGitHubUser promise.
265+ */
266+ var upsertGitHubUser = function ( accessToken , githubData ) {
267+ var query = new Parse . Query ( TokenStorage ) ;
268+ query . equalTo ( 'githubId' , githubData . id ) ;
269+ query . ascending ( 'createdAt' ) ;
270+ // Check if this githubId has previously logged in, using the master key
271+ return query . first ( { useMasterKey : true } ) . then ( function ( tokenData ) {
272+ // If not, create a new user.
273+ if ( ! tokenData ) {
274+ return newGitHubUser ( accessToken , githubData ) ;
275+ }
276+ // If found, fetch the user.
277+ var user = tokenData . get ( 'user' ) ;
278+ return user . fetch ( { useMasterKey : true } ) . then ( function ( user ) {
279+ // Update the accessToken if it is different.
280+ if ( accessToken !== tokenData . get ( 'accessToken' ) ) {
281+ tokenData . set ( 'accessToken' , accessToken ) ;
282+ }
283+ /**
284+ * This save will not use an API request if the token was not changed.
285+ * e.g. when a new user is created and upsert is called again.
286+ */
287+ return tokenData . save ( null , { useMasterKey : true } ) ;
288+ } ) . then ( function ( obj ) {
289+ // Return the user object.
290+ return Parse . Promise . as ( user ) ;
291+ } ) ;
292+ } ) ;
293+ }
294+
295+ /**
296+ * This function creates a Parse User with a random login and password, and
297+ * associates it with an object in the TokenStorage class.
298+ * Once completed, this will return upsertGitHubUser. This is done to protect
299+ * against a race condition: In the rare event where 2 new users are created
300+ * at the same time, only the first one will actually get used.
301+ */
302+ var newGitHubUser = function ( accessToken , githubData ) {
303+ var user = new Parse . User ( ) ;
304+ // Generate a random username and password.
305+ var username = new Buffer ( 24 ) ;
306+ var password = new Buffer ( 24 ) ;
307+ _ . times ( 24 , function ( i ) {
308+ username . set ( i , _ . random ( 0 , 255 ) ) ;
309+ password . set ( i , _ . random ( 0 , 255 ) ) ;
310+ } ) ;
311+ user . set ( "username" , username . toString ( 'base64' ) ) ;
312+ user . set ( "password" , password . toString ( 'base64' ) ) ;
313+ // Sign up the new User
314+ return user . signUp ( ) . then ( function ( user ) {
315+ // create a new TokenStorage object to store the user+GitHub association.
316+ var ts = new TokenStorage ( ) ;
317+ ts . set ( 'githubId' , githubData . id ) ;
318+ ts . set ( 'githubLogin' , githubData . login ) ;
319+ ts . set ( 'accessToken' , accessToken ) ;
320+ ts . set ( 'user' , user ) ;
321+ ts . setACL ( restrictedAcl ) ;
322+ // Use the master key because TokenStorage objects should be protected.
323+ return ts . save ( null , { useMasterKey : true } ) ;
324+ } ) . then ( function ( tokenStorage ) {
325+ return upsertGitHubUser ( accessToken , githubData ) ;
326+ } ) ;
327+ }
0 commit comments