Skip to content
This repository was archived by the owner on Jun 12, 2018. It is now read-only.

Commit f3cdb6d

Browse files
author
Fosco Marotto
committed
Release of third-party authentication tutorial.
0 parents  commit f3cdb6d

File tree

7 files changed

+428
-0
lines changed

7 files changed

+428
-0
lines changed

cloud/main.js

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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+
}

cloud/views/error.ejs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<% include head %>
2+
<p>An error has occurred: <%= errorMessage %></p>
3+
<p><a href="/authorize">Login with GitHub</a></p>
4+
<% include foot %>

cloud/views/foot.ejs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
</div>
2+
</body>
3+
</html>

cloud/views/head.ejs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width" />
6+
7+
<title>GitHub Authentication Example</title>
8+
9+
<script src="//code.jquery.com/jquery-2.0.3.min.js"></script>
10+
<script src="//www.parsecdn.com/js/parse-1.2.13.min.js"></script>
11+
<script src="/js/base-app.js"></script>
12+
<script type="text/javascript">
13+
Parse.initialize(
14+
"your-parse-application-id-here",
15+
"your-parse-javascript-key-here"
16+
);
17+
<body>
18+
<div class="container">
19+
<h1 class="intro">GitHub Authentication Example</h1>

cloud/views/login.ejs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<% include head %>
2+
<p><a href="/authorize">Login with GitHub</a></p>
3+
<script type="text/javascript">
4+
if (Parse.User.current()) {
5+
window.location.href='/main';
6+
}
7+
</script>
8+
<% include foot %>

0 commit comments

Comments
 (0)