Open philiphacks opened 12 years ago
Anyone? I used the LinkedIn OAuth verification test console (https://developer.linkedin.com/oauth-test-console) to see if the signatures match with my POST request, but they don't... :/ I am sure all my keys, access tokens etc are correct. Using Apigee, I can successfully send a message, so maybe the OAuth library is bugged? Can someone look into this issue? :)
Hi @philipdesmedt, i am having the same issue and it's getting me mad
have you fixed that?
thanks!
Yes, I did fix it, but it's a (kind of ugly) hack. Let me quickly explain. I might do a pull request if I've got some time the next few days. Basically, the included OAuth library doesn't work as it should with POST requests.
So, I wanted to keep using this API wrapper and make POST requests as well. After a while, I noticed the OAuth library just did not work with POST requests, as it generated the wrong authorization 'string' (which is a combination of a consumer key, nonce, signature method, timestamp, token, version and signature). I implemented the authorization and signature method myself and passed this to the linkedin-js wrapper. I used the following code:
messageConstructor.prototype._getAuthorization = function(token, secret) {
var oauth_header = 'OAuth ';
oauth_header += 'oauth_consumer_key=app.linkedin.consumer,';
var timestamp = OAuth.timestamp(),
nonce = getNonce();
oauth_header += 'oauth_nonce="' + nonce + '",';
oauth_header += 'oauth_signature_method="HMAC-SHA1",';
oauth_header += 'oauth_timestamp="' + timestamp + '",';
oauth_header += 'oauth_token="' + token + '",';
oauth_header += 'oauth_version="1.0",';
oauth_header += 'oauth_signature="' + this._getSignature(token, secret, timestamp, nonce) + '"';
return oauth_header;
};
messageConstructor.prototype._getSignature = function(token, secret, timestamp, nonce) {
var signature_basestring_parameters = {
oauth_version: '1.0',
oauth_consumer_key: app.linkedin.consumer,
oauth_timestamp: timestamp,
oauth_nonce: nonce,
oauth_token: token,
oauth_signature_method: 'HMAC-SHA1'
},
signature_basestring = OAuth.SignatureMethod.getBaseString({
method: 'POST',
action: 'http://api.linkedin.com/v1/people/~/mailbox',
parameters: signature_basestring_parameters
});
var signer = OAuth.SignatureMethod.newMethod('HMAC-SHA1', {
consumerSecret: app.linkedin.consumer,
tokenSecret: secret
});
var oauth_signature = signer.getSignature(signature_basestring); //signature validated using example from http://oauth.net/core/1.0a/#sig_base_example
oauth_signature = OAuth.percentEncode(oauth_signature); //percent encoded signature validated using example from http
log.insecure('_getSignature: ' + 'generated signature: ' + oauth_signature +
' nonce: ' + nonce + ' timestamp: ' + timestamp);
return oauth_signature;
};
function getNonce() {
return OAuth.nonce(19);
}
I used the above methods to generate the authorization and signature strings. The code to send the message through linkedin itself looked something like this:
messageConstructor.prototype.sendMessage = function (recipient_id, subject, message, callback) {
f = ':sendMessage:';
var self = this;
log.finer(__filename+f+'Getting LITokens');
this.userManager.getLITokens(this.email, function (err, doc) {
if (err) {
log.error(__filename+f+'Failed getting LITokens. Error=('+util.inspect(err, false, null));
} else if (doc !== null) {
log.finer(__filename+f+'Successfully received LITokens');
log.finest(__filename+f+'Doc=('+util.inspect(doc, false, null)+')');
var token = crypto.decrypt(doc.token);
var secret = crypto.decrypt(doc.secret);
log.insecure('token = ' + token);
log.insecure('secret = ' + secret);
var authorization = self._getAuthorization(token, secret);
linkedin.apiCall('POST', '/people/~/mailbox',
{
token: {
oauth_token: token,
oauth_token_secret: secret
},
authorization: authorization,
'mailbox-item': {
'recipients' : {
'values' :
[
{
'person' : {
'_path' : '/people/'+recipient_id
}
}
]
},
'subject' : subject,
'body' : message
}
},
function (error, result) {
if (error) {
log.error(__filename+f+'Failed sending message. error= ' + util.inspect(error, false, null));
callback(error);
} else {
log.finer(__filename+f+'Successfully sent message to id ' + recipient_id);
log.finest(__filename+f+'Result=('+util.inspect(result, false, null)+')');
callback(null, result);
}
}
);
} else {
log.error(__filename+f+'Failed getting LITokens. Doc is undefined');
}
});
};
In the above, the linkedin object is just an instance of this linkedin library. As you can see, I pass an HTTP method, a URL, parameters and callback. The parameters include the authorization string. Ok... almost there. Next I modified the linkedin-js library itself to intercept this authorization string and - if not null - use it instead of the authorization string it generates. Still following? :+1:
In linkedin_client.js, starting from line 75:
else if (method.toUpperCase() === 'POST') {
return CLIENT.oauth.post(
_rest_base + path
, token.oauth_token
, token.oauth_token_secret
, JSON.stringify(params['mailbox-item'])
, 'application/json; charset=UTF-8'
, requestCallback(callback)
, authorization
);
}
As you can see, here I caught the authorization string and I JSON stringified the mailbox-item immediately (normally params should already be a JSON object, I guess). Another reason I didn't do a pull request... my solution only worked for mailbox items now, since I hardcoded that. Oh well. Finally, in /linkedin-js/node_modules/oauth/lib/oauth.js, I changed quite a few methods:
exports.OAuth.prototype._putOrPost= function(method, url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization) {
var extra_params= null;
if( typeof post_content_type == "function" ) {
callback= post_content_type;
post_content_type= null;
}
if( typeof post_body != "string" ) {
post_content_type= "application/x-www-form-urlencoded"
extra_params= post_body;
post_body= null;
}
return this._performSecureRequest( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback, authorization );
}
exports.OAuth.prototype.put= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback) {
return this._putOrPost("PUT", null, url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization);
}
exports.OAuth.prototype.post= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization) {
return this._putOrPost("POST", url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization);
}
As you can see, I added the authorization string as a parameter to these methods and changed each method call all the way up to _performSecureRequest, where I did an (authorization === undefined) check. If it was undefined, I just calculated it using _buildAuthorizationHeaders, else I used the one that I calculated myself and generated. I probably spend hours and hours looking at this and in the end I was quite desperate, so that's why I tried calculating my own authorization string. Still a wonder that this worked. I'm not proud of the solution and I should've changed it once I noticed that this worked, but I guess other functionality was more important at that time.
A better (read: no monkey patching) solution would have been to contact @masylum and replace the OAuth library itself, so POST requests work again. AFAIK, that is the solution. I'm not sure if this project is still under maintenance though. If you've got other questions, feel free to ask and I'll try to respond in a timely manner.
Thanks a lot! you are very kind, i didn't expect such a great explanation i'll try again with your great colaboration thanks again and have a great day Juan
2013/3/19 Philip De Smedt notifications@github.com
Yes, I did fix it, but it's a (kind of ugly) hack. Let me quickly explain. I might do a pull request if I've got some time the next few days. Basically, the included OAuth library doesn't work as it should with POST requests.
So, I wanted to keep using this API wrapper and make POST requests as well. After a while, I noticed the OAuth library just did not work with POST requests, as it generated the wrong authorization 'string' (which is a combination of a consumer key, nonce, signature method, timestamp, token, version and signature). I implemented the authorization and signature method myself and passed this to the linkedin-js wrapper. I used the following code:
messageConstructor.prototype._getAuthorization = function(token, secret) { var oauth_header = 'OAuth '; oauth_header += 'oauth_consumer_key=app.linkedin.consumer,';
var timestamp = OAuth.timestamp(), nonce = getNonce(); oauth_header += 'oauth_nonce="' + nonce + '",'; oauth_header += 'oauth_signature_method="HMAC-SHA1",'; oauth_header += 'oauth_timestamp="' + timestamp + '",'; oauth_header += 'oauth_token="' + token + '",'; oauth_header += 'oauth_version="1.0",'; oauth_header += 'oauth_signature="' + this._getSignature(token, secret, timestamp, nonce) + '"'; return oauth_header;
};
messageConstructor.prototype._getSignature = function(token, secret, timestamp, nonce) { var signature_basestring_parameters = { oauth_version: '1.0', oauth_consumer_key: app.linkedin.consumer, oauth_timestamp: timestamp, oauth_nonce: nonce, oauth_token: token, oauth_signature_method: 'HMAC-SHA1' }, signature_basestring = OAuth.SignatureMethod.getBaseString({ method: 'POST', action: 'http://api.linkedin.com/v1/people/~/mailbox', parameters: signature_basestring_parameters });
var signer = OAuth.SignatureMethod.newMethod('HMAC-SHA1', { consumerSecret: app.linkedin.consumer, tokenSecret: secret }); var oauth_signature = signer.getSignature(signature_basestring); //signature validated using example from http://oauth.net/core/1.0a/#sig_base_example oauth_signature = OAuth.percentEncode(oauth_signature); //percent encoded signature validated using example from http log.insecure('_getSignature: ' + 'generated signature: ' + oauth_signature + ' nonce: ' + nonce + ' timestamp: ' + timestamp); return oauth_signature;
};
function getNonce() { return OAuth.nonce(19); }
I used the above methods to generate the authorization and signature strings. The code to send the message through linkedin itself looked something like this:
messageConstructor.prototype.sendMessage = function (recipient_id, subject, message, callback) { f = ':sendMessage:'; var self = this; log.finer(filename+f+'Getting LITokens'); this.userManager.getLITokens(this.email, function (err, doc) { if (err) { log.error(filename+f+'Failed getting LITokens. Error=('+util.inspect(err, false, null)); } else if (doc !== null) { log.finer(filename+f+'Successfully received LITokens'); log.finest(filename+f+'Doc=('+util.inspect(doc, false, null)+')'); var token = crypto.decrypt(doc.token); var secret = crypto.decrypt(doc.secret); log.insecure('token = ' + token); log.insecure('secret = ' + secret); var authorization = self._getAuthorization(token, secret); linkedin.apiCall('POST', '/people/~/mailbox', { token: { oauth_token: token, oauth_token_secret: secret }, authorization: authorization, 'mailbox-item': { 'recipients' : { 'values' : [ { 'person' : { '_path' : '/people/'+recipient_id } } ] }, 'subject' : subject, 'body' : message } }, function (error, result) { if (error) { log.error(filename+f+'Failed sending message. error= ' + util.inspect(error, false, null)); callback(error); } else { log.finer(filename+f+'Successfully sent message to id ' + recipient_id); log.finest(filename+f+'Result=('+util.inspect(result, false, null)+')'); callback(null, result); } } ); } else { log.error(filename+f+'Failed getting LITokens. Doc is undefined'); } }); };
In the above, the linkedin object is just an instance of this linkedin library. As you can see, I pass an HTTP method, a URL, parameters and callback. The parameters include the authorization string. Ok... almost there. Next I modified the linkedin-js library itself to intercept this authorization string and - if not null - use it instead of the authorization string it generates. Still following? [image: :+1:]
In linkedin_client.js, starting from line 75:
else if (method.toUpperCase() === 'POST') { return CLIENT.oauth.post( _rest_base + path , token.oauth_token , token.oauth_token_secret , JSON.stringify(params['mailbox-item']) , 'application/json; charset=UTF-8' , requestCallback(callback) , authorization ); }
As you can see, here I caught the authorization string and I JSON stringified the mailbox-item immediately (normally params should already be a JSON object, I guess). Another reason I didn't do a pull request... my solution only worked for mailbox items now, since I hardcoded that. Oh well. Finally, in /linkedin-js/node_modules/oauth/lib/oauth.js, I changed quite a few methods:
exports.OAuth.prototype._putOrPost= function(method, url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization) { var extra_params= null; if( typeof post_content_type == "function" ) { callback= post_content_type; post_content_type= null; } if( typeof post_body != "string" ) { post_content_type= "application/x-www-form-urlencoded" extra_params= post_body; post_body= null; } return this._performSecureRequest( oauth_token, oauth_token_secret, method, url, extra_params, post_body, post_content_type, callback, authorization ); }
exports.OAuth.prototype.put= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback) { return this._putOrPost("PUT", null, url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization); }
exports.OAuth.prototype.post= function(url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization) { return this._putOrPost("POST", url, oauth_token, oauth_token_secret, post_body, post_content_type, callback, authorization); }
As you can see, I added the authorization string as a parameter to these methods and changed each method call all the way up to _performSecureRequest, where I did an (authorization === undefined) check. If it was undefined, I just calculated it using _buildAuthorizationHeaders, else I used the one that I calculated myself and generated. I probably spend hours and hours looking at this and in the end I was quite desperate, so that's why I tried calculating my own authorization string. Still a wonder that this worked. I'm not proud of the solution and I should've changed it once I noticed that this worked, but I guess other functionality was more important at that time.
A better (read: no monkey patching) solution would have been to contact @masylum https://github.com/masylum and replace the OAuth library itself, so POST requests work again. AFAIK, that is the solution. I'm not sure if this project is still under maintenance though. If you've got other questions, feel free to ask and I'll try to respond in a timely manner.
Reply to this email directly or view it on GitHubhttps://github.com/masylum/linkedin-js/issues/9#issuecomment-15143275 .
Guys, may you help me, I am trying to make it work, so thank you very much for your posts, very self-explanatory. But I am stuck with the line, which contains OAuth.timestamp(). Node says object OAuth doesn't have this method. I thought OAuth = require('oauth').OAuth, but it doesn't seem to be true. What is that magic OAuth variable?
Hi Igor, I've not tried this code but it seems that the method is OAuth._getTimestamp() . I see that at https://github.com/ciaranj/node-oauth/blob/master/lib/oauth.js#L57
i hope it fixes your problem, good luck
Juan
2013/4/11 Igor Pavlov notifications@github.com
Guys, may you help me, I am trying to make it work, so thank you very much for your posts, very self-explanatory. But I am stuck with the line, which contains OAuth.timestamp(). Node says object OAuth doesn't have this method. I thought OAuth = require('oauth').OAuth, but it doesn't seem to be true. What is that magic OAuth variable?
— Reply to this email directly or view it on GitHubhttps://github.com/masylum/linkedin-js/issues/9#issuecomment-16231370 .
Thanks, I will try it!
When making a POST request (i.e. sharing something or trying to send a message), I get a 401 error:
error: [Messaging] error: { statusCode: 401, data: '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n\n 401 \n 1336572233851 \n Q9YREZSZ2Z \n 0 \n [unauthorized]. OAU:blablabla (token etc)= \n \n' }
When trying to do a GET request, everything goes fine (i.e. getting my connections). Here is the code for the POST request:
I don't know if this is a real issue (i.e. bug) with the library or something else is wrong. The token and secret definitely work (get the same error when I hard-code them unencrypted). Someone who knows what's wrong?