Skip to content

Commit ccc1d02

Browse files
committed
Merge pull request #247 from flovilmart/fix-241
Adds generic support additional OAuth providers
2 parents c7250aa + 077b977 commit ccc1d02

20 files changed

+1101
-87
lines changed

README.md

+55
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,61 @@ The client keys used with Parse are no longer necessary with parse-server. If y
3636
* restAPIKey
3737
* dotNetKey
3838

39+
#### OAuth Support
40+
41+
parse-server supports 3rd party authentication with
42+
43+
* Twitter
44+
* Meetup
45+
* Linkedin
46+
* Google
47+
* Instagram
48+
* Facebook
49+
50+
51+
Configuration options for these 3rd-party modules is done with the oauth option passed to ParseServer:
52+
53+
```
54+
{
55+
oauth: {
56+
twitter: {
57+
consumer_key: "", // REQUIRED
58+
consumer_secret: "" // REQUIRED
59+
},
60+
facebook: {
61+
appIds: "FACEBOOK APP ID"
62+
}
63+
}
64+
65+
}
66+
```
67+
68+
#### Custom Authentication
69+
70+
It is possible to leverage the OAuth support with any 3rd party authentication that you bring in.
71+
72+
```
73+
{
74+
75+
oauth: {
76+
my_custom_auth: {
77+
module: "PATH_TO_MODULE" // OR object,
78+
option1: "",
79+
option2: "",
80+
}
81+
}
82+
}
83+
```
84+
85+
On this module, you need to implement and export those two functions `validateAuthData(authData, options) {} ` and `validateAppId(appIds, authData) {}`.
86+
87+
For more informations about custom auth please see the examples:
88+
89+
- [facebook OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/facebook.js)
90+
- [twitter OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/twitter.js)
91+
- [instagram OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/instagram.js)
92+
93+
3994
#### Advanced options:
4095

4196
* filesAdapter - The default behavior (GridStore) can be changed by creating an adapter class (see [`FilesAdapter.js`](https://github.com/ParsePlatform/parse-server/blob/master/src/Adapters/Files/FilesAdapter.js))

bin/parse-server

+5
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

+307
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)