Allow OAuth1 callback to specify query string parameters.

They are then parsed and provided to underlying HTTP package. We have to
parse them so that signing of requests works properly. In addition,
nonce used in the request is stored in the response so that user can
verify JWT payloads returned from the server.
This commit is contained in:
Mitar
2014-08-14 07:32:08 -07:00
committed by Emily Stark
parent f986056d0c
commit 5853f2aefd
2 changed files with 53 additions and 8 deletions

View File

@@ -1,5 +1,6 @@
var crypto = Npm.require("crypto"); var crypto = Npm.require("crypto");
var querystring = Npm.require("querystring"); var querystring = Npm.require("querystring");
var urlModule = Npm.require("url");
// An OAuth1 wrapper around http calls which helps get tokens and // An OAuth1 wrapper around http calls which helps get tokens and
// takes care of HTTP headers // takes care of HTTP headers
@@ -27,9 +28,9 @@ OAuth1Binding.prototype.prepareRequestToken = function(callbackUrl) {
var response = self._call('POST', self._urls.requestToken, headers); var response = self._call('POST', self._urls.requestToken, headers);
var tokens = querystring.parse(response.content); var tokens = querystring.parse(response.content);
if (!tokens.oauth_callback_confirmed) if (! tokens.oauth_callback_confirmed)
throw new Error( throw _.extend(new Error("oauth_callback_confirmed false when requesting oauth1 token"),
"oauth_callback_confirmed false when requesting oauth1 token", tokens); {response: response});
self.requestToken = tokens.oauth_token; self.requestToken = tokens.oauth_token;
self.requestTokenSecret = tokens.oauth_token_secret; self.requestTokenSecret = tokens.oauth_token_secret;
@@ -55,6 +56,15 @@ OAuth1Binding.prototype.prepareAccessToken = function(query, requestTokenSecret)
var response = self._call('POST', self._urls.accessToken, headers); var response = self._call('POST', self._urls.accessToken, headers);
var tokens = querystring.parse(response.content); var tokens = querystring.parse(response.content);
if (! tokens.oauth_token || ! tokens.oauth_token_secret) {
var error = new Error("missing oauth token or secret");
// We provide response only if no token is available, we do not want to leak any tokens
if (! tokens.oauth_token && ! tokens.oauth_token_secret) {
_.extend(error, {response: response});
}
throw error;
}
self.accessToken = tokens.oauth_token; self.accessToken = tokens.oauth_token;
self.accessTokenSecret = tokens.oauth_token_secret; self.accessTokenSecret = tokens.oauth_token_secret;
}; };
@@ -66,7 +76,7 @@ OAuth1Binding.prototype.call = function(method, url, params, callback) {
oauth_token: self.accessToken oauth_token: self.accessToken
}); });
if(!params) { if(! params) {
params = {}; params = {};
} }
@@ -123,6 +133,20 @@ OAuth1Binding.prototype._call = function(method, url, headers, params, callback)
url = url(self); url = url(self);
} }
headers = headers || {};
params = params || {};
// Extract all query string parameters from the provided URL
var parsedUrl = urlModule.parse(url, true);
// Merge them in a way that params given to the method call have precedence
params = _.extend({}, parsedUrl.query, params);
// Reconstruct the URL back without any query string parameters
// (they are now in params)
parsedUrl.query = {};
parsedUrl.search = '';
url = urlModule.format(parsedUrl);
// Get the signature // Get the signature
headers.oauth_signature = headers.oauth_signature =
self._getSignature(method, url, headers, self.accessTokenSecret, params); self._getSignature(method, url, headers, self.accessTokenSecret, params);
@@ -132,12 +156,21 @@ OAuth1Binding.prototype._call = function(method, url, headers, params, callback)
// Make signed request // Make signed request
try { try {
return HTTP.call(method, url, { var response = HTTP.call(method, url, {
params: params, params: params,
headers: { headers: {
Authorization: authString Authorization: authString
} }
}, callback); }, callback && function (error, response) {
if (! error) {
response.nonce = headers.oauth_nonce;
}
callback(error, response);
});
// We store nonce so that JWTs can be validated
if (response)
response.nonce = headers.oauth_nonce;
return response;
} catch (err) { } catch (err) {
throw _.extend(new Error("Failed to send OAuth1 request to " + url + ". " + err.message), throw _.extend(new Error("Failed to send OAuth1 request to " + url + ". " + err.message),
{response: err.response}); {response: err.response});

View File

@@ -1,8 +1,10 @@
var url = Npm.require("url");
// connect middleware // connect middleware
OAuth._requestHandlers['1'] = function (service, query, res) { OAuth._requestHandlers['1'] = function (service, query, res) {
var config = ServiceConfiguration.configurations.findOne({service: service.serviceName}); var config = ServiceConfiguration.configurations.findOne({service: service.serviceName});
if (!config) { if (! config) {
throw new ServiceConfiguration.ConfigError(service.serviceName); throw new ServiceConfiguration.ConfigError(service.serviceName);
} }
@@ -29,7 +31,13 @@ OAuth._requestHandlers['1'] = function (service, query, res) {
if(typeof urls.authenticate === "function") { if(typeof urls.authenticate === "function") {
redirectUrl = urls.authenticate(oauthBinding); redirectUrl = urls.authenticate(oauthBinding);
} else { } else {
redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; // Parse the URL to support additional query parameters in urls.authenticate
var redirectUrlObj = url.parse(urls.authenticate, true);
redirectUrlObj.query = redirectUrlObj.query || {};
redirectUrlObj.query.oauth_token = oauthBinding.requestToken;
redirectUrlObj.search = '';
// Reconstruct the URL back with provided query parameters merged with oauth_token
redirectUrl = url.format(redirectUrlObj);
} }
// redirect to provider login, which will redirect back to "step 2" below // redirect to provider login, which will redirect back to "step 2" below
@@ -42,6 +50,10 @@ OAuth._requestHandlers['1'] = function (service, query, res) {
// Get the user's request token so we can verify it and clear it // Get the user's request token so we can verify it and clear it
var requestTokenInfo = OAuth._retrieveRequestToken(query.state); var requestTokenInfo = OAuth._retrieveRequestToken(query.state);
if (! requestTokenInfo) {
throw new Error("Unable to retrieve request token");
}
// Verify user authorized access and the oauth_token matches // Verify user authorized access and the oauth_token matches
// the requestToken from previous step // the requestToken from previous step
if (query.oauth_token && query.oauth_token === requestTokenInfo.requestToken) { if (query.oauth_token && query.oauth_token === requestTokenInfo.requestToken) {