Twitter REST API: Bad Authentication data - javascript

I'm trying to post a tweet but for any reason it doesn't work as expected.
I suspect that the issue could be related to the signature string, but following what twitter says according to signing requests looks ok.
Here is my code:
function postTweet(user_id, AccessToken, AccessTokenSecret) {
var base_url = 'https://api.twitter.com/1.1/statuses/update.json',
oauth_nonce = randomString(),
oauth_signature,
oauth_timestamp = Math.floor(new Date().getTime() / 1000),
reqArray,
req,
signature_base_string,
signing_key;
reqArray = [
"include_entities=true",
'oauth_consumer_key="' + CONFIG.TWITTER_CONSUMER_KEY + '"',
'oauth_nonce="' + oauth_nonce + '"',
'oauth_signature_method="HMAC-SHA1"',
'oauth_timestamp="' + oauth_timestamp + '"',
'oauth_token="' + AccessToken + '"',
'oauth_version="1.0"',
'status=' + encodeURIComponent("hello world")
];
req = reqArray.sort().join('&');
signature_base_string = "POST&" + encodeURIComponent(base_url) + "&" + encodeURIComponent(req);
signing_key = CONFIG.TWITTER_CONSUMER_KEY_SECRET + '&' + AccessTokenSecret;
oauth_signature = encodeURIComponent(CryptoJS.HmacSHA1(signature_base_string, signing_key).toString(CryptoJS.enc.Base64));
return $http.post('https://api.twitter.com/1.1/statuses/update.json', {
status: 'hello world'
}).then(function (response) {
return response;
}).catch(function (error) {
console.log(error);
});
}
As a response, I get that:
UPDATE
considering that in my project I already have $cordovaOauthUtility I started using it this way:
function postTweet(accessToken, accessTokenSecret) {
var params, signature;
params = {
include_entities: true,
oauth_consumer_key: CONFIG.TWITTER_CONSUMER_KEY,
oauth_nonce: $cordovaOauthUtility.createNonce(10),
oauth_signature_method: "HMAC-SHA1",
oauth_token: accessToken,
oauth_timestamp: Math.round((new Date()).getTime() / 1000.0),
oauth_version: "1.0"
};
signature = $cordovaOauthUtility.createSignature('POST', 'https://api.twitter.com/1.1/statuses/update.json', params, { status: "hello" }, CONFIG.TWITTER_CONSUMER_KEY_SECRET, accessTokenSecret);
return $http.post('https://api.twitter.com/1.1/statuses/update.json', {
status: "hello"
}, {
headers: {
Authorization: signature.authorization_header
}
})
.then(function (response) {
return response;
}).catch(function (error) {
console.log(error);
});
}
UPDATE 2
After trying all the posibilities, the problem persist. Here I paste a plnkr where I have my code.

You are using crypto's HmacSHA256 but sending HMAC-SHA1 as the oauth_signature_method parameter which is the twitter one.
You should probably change your code to
oauth_signature = CryptoJS.HmacSHA1(signature_base_string, signing_key).toString(CryptoJS.enc.Base64);
If you look at your authorization header, you can also notice that something is wrong with it. Indeed, you can see that the oauth_nonce and the oauth_version are prefixed by a & sign, which shouldn't be the case and most likely mean to the api you are not specifying them. It probably comes from the fact you are using the same reqArray to construct both the signature and the header, or your code is not updated.
You also probably don't want to change the global headers sent from your app, in case another request is sent to another api at the same time. Rather, you should send this authorization header only for this specific xhr.
return $http.post('https://api.twitter.com/1.1/statuses/update.json', {
status: 'hello world',
}, {
headers: {
Authorization: auth,
},
})

Well, you're clearly adding oauth_token in your request array but it didn't show up in the screenshot? Is the AccessToken in the params undefined?
EDIT
According to the documentation, we must append double quotes to the headers. Try this?
reqArray = [
"include_entities=true",
'oauth_consumer_key="'+CONFIG.TWITTER_CONSUMER_KEY+'"',
'oauth_nonce="'+oauth_nonce+'"',
'oauth_signature_method="HMAC-SHA1"',
'oauth_timestamp="'+oauth_timestamp+'"',
'oauth_token="'+AccessToken+'"',
'oauth_version="1.0"',
'status='+encodeURIComponent("hello world")
];

Yikes.
I've downloaded your plnkr bundle and added a read only application key set. I only had to set up and make one change to get a {"request":"\/1.1\/statuses\/update.json","error":"Read-only application cannot POST."} response. Initially I was receiving {"errors":[{"code":32,"message":"Could not authenticate you."}]}.
Remove status: "hello" from between the curly brackets { } where you create your signature.
signature = $cordovaOauthUtility.createSignature('POST', 'https://api.twitter.com/1.1/statuses/update.json', params, { }, twitter.consumer_secret, twitter.access_token_secret);
My request headers become the following:
:authority:api.twitter.com
:method:POST
:path:/1.1/statuses/update.json
:scheme:https
accept:application/json, text/plain, */*
accept-encoding:gzip, deflate, br
accept-language:en-US,en;q=0.8
authorization:OAuth oauth_consumer_key="x",oauth_nonce="QFMmqiasFs",oauth_signature_method="HMAC-SHA1",oauth_token="y",oauth_timestamp="1496340853",oauth_version="1.0",oauth_signature="7Ts91LKcP%2FrYsLcF5WtryCvZQFU%3D"
content-length:18
content-type:application/json;charset=UTF-8
origin:http://localhost
referer:http://localhost/twits/
user-agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36
Googling eventually led me to a tutorial:
Displaying the Twitter Feed within Your Ionic App. I noted his general createTwitterSignature function does not take parameters and tweaked your code similarly.
function createTwitterSignature(method, url) {
var token = angular.fromJson(getStoredToken());
var oauthObject = {
oauth_consumer_key: clientId,
oauth_nonce: $cordovaOauthUtility.createNonce(10),
oauth_signature_method: "HMAC-SHA1",
oauth_token: token.oauth_token,
oauth_timestamp: Math.round((new Date()).getTime() / 1000.0),
oauth_version: "1.0"
};
var signatureObj = $cordovaOauthUtility.createSignature(method, url, oauthObject, {}, clientSecret, token.oauth_token_secret);
$http.defaults.headers.common.Authorization = signatureObj.authorization_header;
}
I've read conflicting things about why there should/shouldn't be other parameters there, but I believe the signature is just supposed to be the basis of access and doesn't hash in every operation you want to perform - see Understanding Request Signing For Oauth 1.0a Providers.

Related

URL parse vs constructor: path missing

I am total beginner in Node.js but I am trying to fix what I thought was a simple issue. I am using the following code example for an AWS Lambda function using Node.js 12 runtime:
function respond(event, context, responseStatus, responseData, physicalResourceId, noEcho) {
var responseBody = JSON.stringify({
Status: responseStatus,
Reason: "See the details in CloudWatch Log Stream: " + context.logStreamName,
PhysicalResourceId: physicalResourceId || context.logStreamName,
StackId: event.StackId,
RequestId: event.RequestId,
LogicalResourceId: event.LogicalResourceId,
NoEcho: noEcho || false,
Data: responseData
});
var https = require("https");
var url = require("url");
var parsedUrl = url.parse(event.ResponseURL);
var options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.path,
method: "PUT",
headers: {
"content-type": "",
"content-length": responseBody.length
}
};
var request = https.request(options, function(response) {
context.done();
});
request.on("error", function(error) {
console.log("send(..) failed executing https.request(..): " + error);
context.done();
});
request.write(responseBody);
request.end();
}
Full source code can be found here: https://github.com/aws-samples/amazon-cloudfront-secure-static-site/blob/7f96cdbcfbd7f94c3ab5a4c028b6bafd10744c83/source/witch/witch.js#L70
My IDE gives me a warning that the URL.parse() method is deprecated and so that I should use the URL constructor instead. So the only change I made is replacing:
var parsedUrl = url.parse(event.ResponseURL);
with
var parsedUrl = new url.URL(event.ResponseURL);
But when I do that, the options.path field ends up missing. What is even more confusing to me is that if I log the parsedUrl variable (passing it through JSON.stringify()), I can see that when I use url.parse(), I get a simple string in parsedUrl:
"https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
But when using the constructor, I can see in the log an object structure with all the expected fields (protocol, hostname, port, even path):
{
"protocol": "https:",
"slashes": true,
"auth": null,
"host": "cloudformation-custom-resource-response-useast1.s3.amazonaws.com",
"port": null,
"hostname": "cloudformation-custom-resource-response-useast1.s3.amazonaws.com",
"hash": null,
"search": "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"query": "XX-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"pathname": "/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff",
"path": "/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff",
"href": "https://cloudformation-custom-resource-response-useast1.s3.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-1%3A123456789012%3Astack/AcmCertificateStack-ABCDEFGHIJKL/00112233-4455-6677-8899-aabbccddeeff%7CCopyCustomResource%7C00112233-4455-6677-8899-aabbccddeeff?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20220101T000000Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=ABCDEFGHIJKLMNOPQRST%2F20220101%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"
}
So if anything the constructor seems to provide a better break down of the URL. I don't why, when I try to copy the parsedUrl.path field to options.path it works when parsedUrl comes from the parse() method but not when it comes from the constructor. The hostname field on the other hand works in both cases.
Any idea what's the issue here?
As indicated by the OP, the use of the url.parse method is discouraged in favor of the WATWG URL API. The legacy .path attribute returns the combined pathname and search components. Although the preferred WATWG URL API does not have the path attribute, the value required by options.path can be constructed by combining the .pathname and .search attributes.

Twilio Autopilot's custom channel Memory payload

I'm creating a web chat and I need to integrate an assistant using Twilio Autopilot, I use Autopilot's custom channels as follows:
I send a POST request to https://channels.autopilot.twilio.com/v2/{AccountSid}/{AssistantSid}/custom/webchat
I fill in these fields in the request data (Language=en-US, UserId=user123, Text=Something)
I add a Memory parameter in the Assistant URL
My backend code:
// Assistant URL Memory parameter
const context = JSON.stringify({
accountHolder: "foo bar",
balance: 123
}, null, "");
// Request params
const urlencoded = new URLSearchParams();
urlencoded.append("Language", "en-US");
urlencoded.append("UserId", message.Author);
urlencoded.append("Text", message.Body);
const requestConfig: AxiosRequestConfig = {
headers: {
"content-type": "application/x-www-form-urlencoded",
"accept": "application/json",
"authorization" : "Basic " + Buffer.from(accountSid + ":" + authToken).toString("base64")
},
method: "POST",
url: `https://channels.autopilot.twilio.com/v2/${accountSid}/${assistantSid}/custom/webchat?Memory=${context}`,
data: urlencoded
};
// Trigger the bot
const twilioResponse = this.httpService.request(requestConfig);
const result = await lastValueFrom(twilioResponse.pipe(map((response) => response.data)));
Twilio function:
exports.handler = async function(context, event, callback) {
console.log(event.Memory);
const userData = JSON.parse(event.Memory); // Memory does not contains my payload !
return callback(null, {
"actions": [
{
"say": `Hello ${userData.accountHolder}, your balance is ${userData.balance}`
},
{
"listen": true
}
]
});
};
My problem is, The Memory parameter is not sent to the function
I had a similar problem trying to pass the payload in and reached out to a Twilio support.
The answer I received was:
"As per the present update from our engineering team, they are not looking at implementing the Memory parameter on V2 endpoint as there are very less users consuming it. You will have to use V1 endpoint for Autopilot if you are looking to use URL Parameter "Memory" for your use case. "
Therefore the URL will need to look like so:
https://channels.autopilot.twilio.com/v1/ACxxx/UAxxx/custom/chat?Memory={"CarModel":"Diablo","CarMake":"Lamborghini","CarYear":"2019"}'
Twilio developer evangelist here.
I would have thought you might want to URL encode the JSON memory when you add it to the URL. Try this:
const context = encodeURIComponent(
JSON.stringify({
accountHolder: "foo bar",
balance: 123
}, null, "")
);

When using PUT method body is not passed using Koa

I am trying to make update function, where a user can put the new data and the data in the server would get updated, a simple task, however, when I try to PUT new data, the body is always undefined.
The data which gets sent:
request: {
method: 'PUT',
url: '/api/v1.0/articles/1',
header: {
'user-agent': 'PostmanRuntime/7.17.1',
accept: '*/*',
'cache-control': 'no-cache',
host: 'localhost:3000',
'accept-encoding': 'gzip, deflate',
'content-length': '98',
connection: 'keep-alive'
}
},
response: {
status: 404,
message: 'Not Found',
header: [Object: null prototype] {}
},
Now I tried passing it as keys using other methods and not RAW method, this is what is inside of the body I am trying to pass:
{
"title": "another article",
"fullText": "again here is some text hereto fill the body"
}
This is the function which should update the data, but it gets undefined from the put request.
router.put("/:id", updateArticle);
function updateArticle(cnx, next) {
let id = parseInt(cnx.params.id);
console.log(cnx);
if (articles[id - 1] != null) {
//articles[id - 1].title = cnx.request.body.title;
cnx.body = {
message:
"Updated Successfully: \n:" + JSON.stringify(updateArticle, null, 4)
};
} else {
cnx.body = {
message:
"Article does not exist: \n:" + JSON.stringify(updateArticle, null, 4)
};
}
}
I am using postman, Body -> Raw | JSON, I do have to mention all other methods work perfectly - delete, create, getAll, getById
With a PUT or POST, the data is in the body of the request. You have to have some code (in your request handler or some middleware) that actually reads the body from the stream and populates the body property for you. If you don't have that, then the data is still sitting in the request stream waiting to be read.
You can see an example of reading it yourself here: https://github.com/koajs/koa/issues/719 or there is pre-built middleware that will do that for you.
Here's are a couple modules that will do that middleware for you:
https://github.com/dlau/koa-body
https://www.npmjs.com/package/koa-body-parser

Cannot authenticate authentication error using twitter api?

I am using twitter api using Auth 1.0 and it is working well when using url = 'https://api.twitter.com/1.1/statuses/home_timeline.json'
but when I am using https://api.twitter.com/1.1/search/tweets.json?q=nasa&result_type=popular api its returning
{
"errors": [
{
"code": 32,
"message": "Could not authenticate you."
}
]
}
Now I am not understanding why it is happening...
Following is the header I am making:
function buildRequestHeader(httpMethod, URL) {
parameters = {
oauth_consumer_key: CONSUMER_KEY,
oauth_token: ACCESS_TOKEN,
oauth_nonce: getNonce(),
oauth_timestamp: getTimestamp(),
oauth_signature_method: 'HMAC-SHA1',
oauth_version: '1.0',
},
signature = oauthSignature.generate(httpMethod, URL, parameters, CONSUMER_SECRET, ACCESS_TOKEN_SECRET,{ encodeSignature: false }),
authString = 'OAuth oauth_consumer_key=' + parameters.oauth_consumer_key + ',oauth_token=' + parameters.oauth_token + ',oauth_signature_method=HMAC-SHA1,oauth_timestamp=' + parameters.oauth_timestamp + ',oauth_nonce=' + parameters.oauth_nonce + ',oauth_version=1.0,oauth_signature=' + encodeURIComponent(signature);
return authString;
console.log('>>>>>>>', authString);
}
It is returning me header for twitter which is working well for url = 'https://api.twitter.com/1.1/statuses/home_timeline.json' but error code 32 for https://api.twitter.com/1.1/search/tweets.json?q=nasa&result_type=popular api.Thank you .Any help will be appr
It's been a while, but I believe that your oath parameters need to appear in the AuthString in alphabetical order by parameter name.
Please check your Twitter Application. Twitter Application. In App details, 'Sign in with Twitter' must be 'enabled'.

Seem to Have the Wrong Content Type When POSTing with Chai-HTTP

I am looking to make use of Chai-HTTP for some testing. Naturally I want to test more than my GETs however I seem to be hitting a major roadblock when attempting to make POSTs.
In an attempt to figure out why my POSTs weren't working I began hitting them against a POST test server.
Here is a POST attempt formatted using an entirely different toolchain (Jasmine-Node and Frisby) for testing (that works just fine):
frisby.create('LOGIN')
.post('http://posttestserver.com/post.php', {
grant_type:'password',
username:'helllo#world.com',
password:'password'
})
.addHeader("Token", "text/plain")
.expectStatus(200)
})
.toss();
Which results in:
Time: Mon, 27 Jun 16 13:40:54 -0700
Source ip: 204.191.154.66
Headers (Some may be inserted by server)
REQUEST_URI = /post.php
QUERY_STRING =
REQUEST_METHOD = POST
GATEWAY_INTERFACE = CGI/1.1
REMOTE_PORT = 19216
REMOTE_ADDR = 204.191.154.66
HTTP_CONNECTION = close
CONTENT_LENGTH = 64
HTTP_HOST = posttestserver.com
HTTP_TOKEN = text/plain
CONTENT_TYPE = application/x-www-form-urlencoded
UNIQUE_ID = V3GPVkBaMGUAAB1Uf04AAAAc
REQUEST_TIME_FLOAT = 1467060054.9575
REQUEST_TIME = 1467060054
Post Params:
key: 'grant_type' value: 'password'
key: 'username' value: 'hello#world.com'
key: 'password' value: 'password'
Empty post body.
Upload contains PUT data:
grant_type=password&username=hello%40world.com&password=password
And here is a POST attempt using Chai and Chai-HTTP. I would expect this to work the same as the above example using Jasmine and Frisby, however, you'll see the actual request differs in several ways.
describe('/post.php', function() {
var endPointUnderTest = '/post.php';
it('should return an auth token', function(done) {
chai.request('http://posttestserver.com')
.post(endPointUnderTest)
.set('Token', 'text/plain')
.send({
grant_type: 'password',
username: 'hello#world.com',
password: 'password'
})
.end(function(err, res) {
console.log(res);
res.should.have.status(200);
done();
});
});
});
Which results in:
Time: Tue, 28 Jun 16 06:55:50 -0700
Source ip: 204.191.154.66
Headers (Some may be inserted by server)
REQUEST_URI = /post.php
QUERY_STRING =
REQUEST_METHOD = POST
GATEWAY_INTERFACE = CGI/1.1
REMOTE_PORT = 1409
REMOTE_ADDR = 204.191.154.66
HTTP_CONNECTION = close
CONTENT_LENGTH = 76
CONTENT_TYPE = application/json
HTTP_TOKEN = text/plain
HTTP_USER_AGENT = node-superagent/2.0.0
HTTP_ACCEPT_ENCODING = gzip, deflate
HTTP_HOST = posttestserver.com
UNIQUE_ID = V3KB5kBaMGUAAErPF6IAAAAF
REQUEST_TIME_FLOAT = 1467122150.9125
REQUEST_TIME = 1467122150
No Post Params.
== Begin post body ==
{"grant_type":"password","username":"hello#world.com","password":"password"}
== End post body ==
Upload contains PUT data:
{"grant_type":"password","username":"hello#world.com","password":"password"}
Notice the difference in CONTENT_TYPE, Post Params and PUT data in particular (I think this is the source of my problem).
Where Jasmine/Frisby would submit the POST using the 'application/x-www-form-urlencoded' format, Chai-HTTP seems to be using the 'application/json' format.
Am I somehow misusing Chai-HTTP's POST capabilities? Or does Chai-HTTP not allow for 'application/x-www-form-urlencoded' POST requests? I do not seem to be able to resolve this and it is the final hurdle for me to jump to make the transition to using a Mocha/Chai toolchain for my testing (which is the goal, I would prefer to not use a different library unless it's absolutely necessary).
Having discussed this further on Chai-HTTP's Git-Hub page, I was able to find out that this is expected behaviour of SuperAgent, the HTTP request library under the hood of Chai-HTTP, which auto-detects the content-type based on what kind of data is contained in the .send() call.
I stumbled across this particular question as well which helped clarify what the difference between content-types actually was.
If anyone else runs into this problem, I've learned that Chai-HTTP's POST requests can be altered quite easily (kudos to meeber's help here) using calls like this:
//Override auto-detection by specifying the header explicitly
.set('content-type', 'application/x-www-form-urlencoded')
//Select the type 'form'
.type('form')
//Pass multiple strings in send instead of using an object
.send('grant_type=password')
.send('username=hello#world.com')
.send('password=password')
Creating a request that looks like this:
describe('/post.php', function() {
var endPointUnderTest = '/post.php';
it('should return an auth token', function(done) {
chai.request('http://posttestserver.com')
.post(endPointUnderTest)
.set('Token', 'text/plain')
.set('content-type', 'application/x-www-form-urlencoded')
.type('form')
.send('grant_type=password')
.send('username=hello#world.com')
.send('password=password')
.end(function(err, res) {
console.log(res);
res.should.have.status(200);
done();
});
});
});

Categories

Resources