Skip to content

Commit e010fd8

Browse files
committed
Generic OAuth provider support
Refactors facebook login into oauth generic login Adds additional oauth2 providers adds ability to pass an oAuth validator in the config Adds Twitter validation support + OAuth 1 client Support auth_token instead of access_token for twitter Improves code coverage of OAuth Adds validation of oauth provider structures Better coverage of the OAuth spec 100% coverage of OAuth1.js Adds passing auth_token_secret for Twitter auth. Refactors auth validation methods to include authData parameter - Adds ability to extens oauth validator through configuration - Adds ability to extend oauth validator through external module (file or package) - Adds more tests - Adds tests to login with custom auth provider Adds more tests for REST API fixes twitter auth_token f
1 parent f8ae863 commit e010fd8

19 files changed

+1061
-87
lines changed

bin/parse-server

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ if (process.env.PARSE_SERVER_OPTIONS) {
3030
facebookAppIds = facebookAppIds.split(",");
3131
options.facebookAppIds = facebookAppIds;
3232
}
33+
34+
var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS;
35+
if (oauth) {
36+
options.oauth = JSON.parse(oauth);
37+
};
3338
}
3439

3540
var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";

spec/OAuth.spec.js

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
var OAuth = require("../src/oauth/OAuth1Client");
2+
var request = require('request');
3+
4+
describe('OAuth', function() {
5+
6+
it("Nonce should have right length", (done) => {
7+
jequal(OAuth.nonce().length, 30);
8+
done();
9+
});
10+
11+
it("Should properly build parameter string", (done) => {
12+
var string = OAuth.buildParameterString({c:1, a:2, b:3})
13+
jequal(string, "a=2&b=3&c=1");
14+
done();
15+
});
16+
17+
it("Should properly build empty parameter string", (done) => {
18+
var string = OAuth.buildParameterString()
19+
jequal(string, "");
20+
done();
21+
});
22+
23+
it("Should properly build signature string", (done) => {
24+
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
25+
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
26+
done();
27+
});
28+
29+
it("Should properly generate request signature", (done) => {
30+
var request = {
31+
host: "dummy.com",
32+
path: "path"
33+
};
34+
35+
var oauth_params = {
36+
oauth_timestamp: 123450000,
37+
oauth_nonce: "AAAAAAAAAAAAAAAAA",
38+
oauth_consumer_key: "hello",
39+
oauth_token: "token"
40+
};
41+
42+
var consumer_secret = "world";
43+
var auth_token_secret = "secret";
44+
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
45+
jequal(request.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="8K95bpQcDi9Nd2GkhumTVcw4%2BXw%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"');
46+
done();
47+
});
48+
49+
it("Should properly build request", (done) => {
50+
var options = {
51+
host: "dummy.com",
52+
consumer_key: "hello",
53+
consumer_secret: "world",
54+
auth_token: "token",
55+
auth_token_secret: "secret",
56+
// Custom oauth params for tests
57+
oauth_params: {
58+
oauth_timestamp: 123450000,
59+
oauth_nonce: "AAAAAAAAAAAAAAAAA"
60+
}
61+
};
62+
var path = "path";
63+
var method = "get";
64+
65+
var oauthClient = new OAuth(options);
66+
var req = oauthClient.buildRequest(method, path, {"query": "param"});
67+
68+
jequal(req.host, options.host);
69+
jequal(req.path, "/"+path+"?query=param");
70+
jequal(req.method, "GET");
71+
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
72+
jequal(req.headers['Authorization'], 'OAuth oauth_consumer_key="hello", oauth_nonce="AAAAAAAAAAAAAAAAA", oauth_signature="wNkyEkDE%2F0JZ2idmqyrgHdvC0rs%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="123450000", oauth_token="token", oauth_version="1.0"')
73+
done();
74+
});
75+
76+
77+
function validateCannotAuthenticateError(data, done) {
78+
jequal(typeof data, "object");
79+
jequal(typeof data.errors, "object");
80+
var errors = data.errors;
81+
jequal(typeof errors[0], "object");
82+
// Cannot authenticate error
83+
jequal(errors[0].code, 32);
84+
done();
85+
}
86+
87+
it("Should fail a GET request", (done) => {
88+
var options = {
89+
host: "api.twitter.com",
90+
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
91+
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
92+
};
93+
var path = "/1.1/help/configuration.json";
94+
var params = {"lang": "en"};
95+
var oauthClient = new OAuth(options);
96+
oauthClient.get(path, params).then(function(data){
97+
validateCannotAuthenticateError(data, done);
98+
})
99+
});
100+
101+
it("Should fail a POST request", (done) => {
102+
var options = {
103+
host: "api.twitter.com",
104+
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
105+
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
106+
};
107+
var body = {
108+
lang: "en"
109+
};
110+
var path = "/1.1/account/settings.json";
111+
112+
var oauthClient = new OAuth(options);
113+
oauthClient.post(path, null, body).then(function(data){
114+
validateCannotAuthenticateError(data, done);
115+
})
116+
});
117+
118+
it("Should fail a request", (done) => {
119+
var options = {
120+
host: "localhost",
121+
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
122+
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
123+
};
124+
var body = {
125+
lang: "en"
126+
};
127+
var path = "/";
128+
129+
var oauthClient = new OAuth(options);
130+
oauthClient.post(path, null, body).then(function(data){
131+
jequal(false, true);
132+
done();
133+
}).catch(function(){
134+
jequal(true, true);
135+
done();
136+
})
137+
});
138+
139+
["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
140+
it("Should validate structure of "+providerName, (done) => {
141+
var provider = require("../src/oauth/"+providerName);
142+
jequal(typeof provider.validateAuthData, "function");
143+
jequal(typeof provider.validateAppId, "function");
144+
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
145+
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
146+
done();
147+
});
148+
});
149+
150+
var getMockMyOauthProvider = function() {
151+
return {
152+
authData: {
153+
id: "12345",
154+
access_token: "12345",
155+
expiration_date: new Date().toJSON(),
156+
},
157+
shouldError: false,
158+
loggedOut: false,
159+
synchronizedUserId: null,
160+
synchronizedAuthToken: null,
161+
synchronizedExpiration: null,
162+
163+
authenticate: function(options) {
164+
if (this.shouldError) {
165+
options.error(this, "An error occurred");
166+
} else if (this.shouldCancel) {
167+
options.error(this, null);
168+
} else {
169+
options.success(this, this.authData);
170+
}
171+
},
172+
restoreAuthentication: function(authData) {
173+
if (!authData) {
174+
this.synchronizedUserId = null;
175+
this.synchronizedAuthToken = null;
176+
this.synchronizedExpiration = null;
177+
return true;
178+
}
179+
this.synchronizedUserId = authData.id;
180+
this.synchronizedAuthToken = authData.access_token;
181+
this.synchronizedExpiration = authData.expiration_date;
182+
return true;
183+
},
184+
getAuthType: function() {
185+
return "myoauth";
186+
},
187+
deauthenticate: function() {
188+
this.loggedOut = true;
189+
this.restoreAuthentication(null);
190+
}
191+
};
192+
};
193+
194+
var ExtendedUser = Parse.User.extend({
195+
extended: function() {
196+
return true;
197+
}
198+
});
199+
200+
var createOAuthUser = function(callback) {
201+
var jsonBody = {
202+
authData: {
203+
myoauth: getMockMyOauthProvider().authData
204+
}
205+
};
206+
var headers = {'X-Parse-Application-Id': 'test',
207+
'X-Parse-REST-API-Key': 'rest',
208+
'Content-Type': 'application/json' }
209+
210+
var options = {
211+
headers: {'X-Parse-Application-Id': 'test',
212+
'X-Parse-REST-API-Key': 'rest',
213+
'Content-Type': 'application/json' },
214+
url: 'http://localhost:8378/1/users',
215+
body: JSON.stringify(jsonBody)
216+
};
217+
218+
return request.post(options, callback);
219+
}
220+
221+
it("should create user with REST API", (done) => {
222+
223+
createOAuthUser((error, response, body) => {
224+
expect(error).toBe(null);
225+
var b = JSON.parse(body);
226+
expect(b.objectId).not.toBeNull();
227+
expect(b.objectId).not.toBeUndefined();
228+
done();
229+
});
230+
231+
});
232+
233+
it("should only create a single user with REST API", (done) => {
234+
var objectId;
235+
createOAuthUser((error, response, body) => {
236+
expect(error).toBe(null);
237+
var b = JSON.parse(body);
238+
expect(b.objectId).not.toBeNull();
239+
expect(b.objectId).not.toBeUndefined();
240+
objectId = b.objectId;
241+
242+
createOAuthUser((error, response, body) => {
243+
expect(error).toBe(null);
244+
var b = JSON.parse(body);
245+
expect(b.objectId).not.toBeNull();
246+
expect(b.objectId).not.toBeUndefined();
247+
expect(b.objectId).toBe(objectId);
248+
done();
249+
});
250+
});
251+
252+
});
253+
254+
it("unlink and link with custom provider", (done) => {
255+
var provider = getMockMyOauthProvider();
256+
Parse.User._registerAuthenticationProvider(provider);
257+
Parse.User._logInWith("myoauth", {
258+
success: function(model) {
259+
ok(model instanceof Parse.User, "Model should be a Parse.User");
260+
strictEqual(Parse.User.current(), model);
261+
ok(model.extended(), "Should have used the subclass.");
262+
strictEqual(provider.authData.id, provider.synchronizedUserId);
263+
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
264+
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
265+
ok(model._isLinked("myoauth"), "User should be linked to myoauth");
266+
267+
model._unlinkFrom("myoauth", {
268+
success: function(model) {
269+
ok(!model._isLinked("myoauth"),
270+
"User should not be linked to myoauth");
271+
ok(!provider.synchronizedUserId, "User id should be cleared");
272+
ok(!provider.synchronizedAuthToken, "Auth token should be cleared");
273+
ok(!provider.synchronizedExpiration,
274+
"Expiration should be cleared");
275+
276+
model._linkWith("myoauth", {
277+
success: function(model) {
278+
ok(provider.synchronizedUserId, "User id should have a value");
279+
ok(provider.synchronizedAuthToken,
280+
"Auth token should have a value");
281+
ok(provider.synchronizedExpiration,
282+
"Expiration should have a value");
283+
ok(model._isLinked("myoauth"),
284+
"User should be linked to myoauth");
285+
done();
286+
},
287+
error: function(model, error) {
288+
ok(false, "linking again should succeed");
289+
done();
290+
}
291+
});
292+
},
293+
error: function(model, error) {
294+
ok(false, "unlinking should succeed");
295+
done();
296+
}
297+
});
298+
},
299+
error: function(model, error) {
300+
ok(false, "linking should have worked");
301+
done();
302+
}
303+
});
304+
});
305+
306+
307+
})

0 commit comments

Comments
 (0)