Skip to content

Commit

Permalink
Merge pull request #247 from flovilmart/fix-241
Browse files Browse the repository at this point in the history
Adds generic support additional OAuth providers
  • Loading branch information
drew-gross committed Feb 17, 2016
2 parents c7250aa + 077b977 commit ccc1d02
Show file tree
Hide file tree
Showing 20 changed files with 1,101 additions and 87 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,61 @@ The client keys used with Parse are no longer necessary with parse-server. If y
* restAPIKey
* dotNetKey

#### OAuth Support

parse-server supports 3rd party authentication with

* Twitter
* Meetup
* Linkedin
* Google
* Instagram
* Facebook


Configuration options for these 3rd-party modules is done with the oauth option passed to ParseServer:

```
{
oauth: {
twitter: {
consumer_key: "", // REQUIRED
consumer_secret: "" // REQUIRED
},
facebook: {
appIds: "FACEBOOK APP ID"
}
}
}
```

#### Custom Authentication

It is possible to leverage the OAuth support with any 3rd party authentication that you bring in.

```
{
oauth: {
my_custom_auth: {
module: "PATH_TO_MODULE" // OR object,
option1: "",
option2: "",
}
}
}
```

On this module, you need to implement and export those two functions `validateAuthData(authData, options) {} ` and `validateAppId(appIds, authData) {}`.

For more informations about custom auth please see the examples:

- [facebook OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/facebook.js)
- [twitter OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/twitter.js)
- [instagram OAuth](https://github.com/ParsePlatform/parse-server/blob/master/src/oauth/instagram.js)


#### Advanced options:

* 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))
Expand Down
5 changes: 5 additions & 0 deletions bin/parse-server
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ if (process.env.PARSE_SERVER_OPTIONS) {
facebookAppIds = facebookAppIds.split(",");
options.facebookAppIds = facebookAppIds;
}

var oauth = process.env.PARSE_SERVER_OAUTH_PROVIDERS;
if (oauth) {
options.oauth = JSON.parse(oauth);
};
}

var mountPath = process.env.PARSE_SERVER_MOUNT_PATH || "/";
Expand Down
307 changes: 307 additions & 0 deletions spec/OAuth.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
var OAuth = require("../src/oauth/OAuth1Client");
var request = require('request');

describe('OAuth', function() {

it("Nonce should have right length", (done) => {
jequal(OAuth.nonce().length, 30);
done();
});

it("Should properly build parameter string", (done) => {
var string = OAuth.buildParameterString({c:1, a:2, b:3})
jequal(string, "a=2&b=3&c=1");
done();
});

it("Should properly build empty parameter string", (done) => {
var string = OAuth.buildParameterString()
jequal(string, "");
done();
});

it("Should properly build signature string", (done) => {
var string = OAuth.buildSignatureString("get", "http://dummy.com", "");
jequal(string, "GET&http%3A%2F%2Fdummy.com&");
done();
});

it("Should properly generate request signature", (done) => {
var request = {
host: "dummy.com",
path: "path"
};

var oauth_params = {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA",
oauth_consumer_key: "hello",
oauth_token: "token"
};

var consumer_secret = "world";
var auth_token_secret = "secret";
request = OAuth.signRequest(request, oauth_params, consumer_secret, auth_token_secret);
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"');
done();
});

it("Should properly build request", (done) => {
var options = {
host: "dummy.com",
consumer_key: "hello",
consumer_secret: "world",
auth_token: "token",
auth_token_secret: "secret",
// Custom oauth params for tests
oauth_params: {
oauth_timestamp: 123450000,
oauth_nonce: "AAAAAAAAAAAAAAAAA"
}
};
var path = "path";
var method = "get";

var oauthClient = new OAuth(options);
var req = oauthClient.buildRequest(method, path, {"query": "param"});

jequal(req.host, options.host);
jequal(req.path, "/"+path+"?query=param");
jequal(req.method, "GET");
jequal(req.headers['Content-Type'], 'application/x-www-form-urlencoded');
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"')
done();
});


function validateCannotAuthenticateError(data, done) {
jequal(typeof data, "object");
jequal(typeof data.errors, "object");
var errors = data.errors;
jequal(typeof errors[0], "object");
// Cannot authenticate error
jequal(errors[0].code, 32);
done();
}

it("Should fail a GET request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var path = "/1.1/help/configuration.json";
var params = {"lang": "en"};
var oauthClient = new OAuth(options);
oauthClient.get(path, params).then(function(data){
validateCannotAuthenticateError(data, done);
})
});

it("Should fail a POST request", (done) => {
var options = {
host: "api.twitter.com",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/1.1/account/settings.json";

var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
validateCannotAuthenticateError(data, done);
})
});

it("Should fail a request", (done) => {
var options = {
host: "localhost",
consumer_key: "XXXXXXXXXXXXXXXXXXXXXXXXX",
consumer_secret: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
};
var body = {
lang: "en"
};
var path = "/";

var oauthClient = new OAuth(options);
oauthClient.post(path, null, body).then(function(data){
jequal(false, true);
done();
}).catch(function(){
jequal(true, true);
done();
})
});

["facebook", "github", "instagram", "google", "linkedin", "meetup", "twitter"].map(function(providerName){
it("Should validate structure of "+providerName, (done) => {
var provider = require("../src/oauth/"+providerName);
jequal(typeof provider.validateAuthData, "function");
jequal(typeof provider.validateAppId, "function");
jequal(provider.validateAuthData({}, {}).constructor, Promise.prototype.constructor);
jequal(provider.validateAppId("app", "key", {}).constructor, Promise.prototype.constructor);
done();
});
});

var getMockMyOauthProvider = function() {
return {
authData: {
id: "12345",
access_token: "12345",
expiration_date: new Date().toJSON(),
},
shouldError: false,
loggedOut: false,
synchronizedUserId: null,
synchronizedAuthToken: null,
synchronizedExpiration: null,

authenticate: function(options) {
if (this.shouldError) {
options.error(this, "An error occurred");
} else if (this.shouldCancel) {
options.error(this, null);
} else {
options.success(this, this.authData);
}
},
restoreAuthentication: function(authData) {
if (!authData) {
this.synchronizedUserId = null;
this.synchronizedAuthToken = null;
this.synchronizedExpiration = null;
return true;
}
this.synchronizedUserId = authData.id;
this.synchronizedAuthToken = authData.access_token;
this.synchronizedExpiration = authData.expiration_date;
return true;
},
getAuthType: function() {
return "myoauth";
},
deauthenticate: function() {
this.loggedOut = true;
this.restoreAuthentication(null);
}
};
};

var ExtendedUser = Parse.User.extend({
extended: function() {
return true;
}
});

var createOAuthUser = function(callback) {
var jsonBody = {
authData: {
myoauth: getMockMyOauthProvider().authData
}
};
var headers = {'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json' }

var options = {
headers: {'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json' },
url: 'http://localhost:8378/1/users',
body: JSON.stringify(jsonBody)
};

return request.post(options, callback);
}

it("should create user with REST API", (done) => {

createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
done();
});

});

it("should only create a single user with REST API", (done) => {
var objectId;
createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
objectId = b.objectId;

createOAuthUser((error, response, body) => {
expect(error).toBe(null);
var b = JSON.parse(body);
expect(b.objectId).not.toBeNull();
expect(b.objectId).not.toBeUndefined();
expect(b.objectId).toBe(objectId);
done();
});
});

});

it("unlink and link with custom provider", (done) => {
var provider = getMockMyOauthProvider();
Parse.User._registerAuthenticationProvider(provider);
Parse.User._logInWith("myoauth", {
success: function(model) {
ok(model instanceof Parse.User, "Model should be a Parse.User");
strictEqual(Parse.User.current(), model);
ok(model.extended(), "Should have used the subclass.");
strictEqual(provider.authData.id, provider.synchronizedUserId);
strictEqual(provider.authData.access_token, provider.synchronizedAuthToken);
strictEqual(provider.authData.expiration_date, provider.synchronizedExpiration);
ok(model._isLinked("myoauth"), "User should be linked to myoauth");

model._unlinkFrom("myoauth", {
success: function(model) {
ok(!model._isLinked("myoauth"),
"User should not be linked to myoauth");
ok(!provider.synchronizedUserId, "User id should be cleared");
ok(!provider.synchronizedAuthToken, "Auth token should be cleared");
ok(!provider.synchronizedExpiration,
"Expiration should be cleared");

model._linkWith("myoauth", {
success: function(model) {
ok(provider.synchronizedUserId, "User id should have a value");
ok(provider.synchronizedAuthToken,
"Auth token should have a value");
ok(provider.synchronizedExpiration,
"Expiration should have a value");
ok(model._isLinked("myoauth"),
"User should be linked to myoauth");
done();
},
error: function(model, error) {
ok(false, "linking again should succeed");
done();
}
});
},
error: function(model, error) {
ok(false, "unlinking should succeed");
done();
}
});
},
error: function(model, error) {
ok(false, "linking should have worked");
done();
}
});
});


})
Loading

0 comments on commit ccc1d02

Please sign in to comment.