Twitter request from Parse Cloud Code fails with "Could not authenticate you." - javascript

{"errors":[{"code":32,"message":"Could not authenticate you."}]}
This is what I get when trying to perform a GET users/show request to Twitter. Some background:
User is authenthicated in my Android app through ParseTwitterUtils;
From Android, I call a parse.com Cloud Code function passing in the user token and token secret (looks like bad practice, but for now I'd just like to see this work);
From Cloud Code, I format the auth header using this github library. This is needed as explained here.
You can see some of my code below. Android launch code:
HashMap<String, Object> params = new HashMap<>();
params.put("twitterId", ParseTwitterUtils.getTwitter().getUserId());
params.put("authToken", ParseTwitterUtils.getTwitter().getAuthToken());
params.put("authTokenSecret", ParseTwitterUtils.getTwitter().getAuthTokenSecret());
ParseCloud.callFunctionInBackground("fetchPictureFromTwitter", params, ... );
Cloud code main function:
Parse.Cloud.define("fetchPictureFromTwitter", function(request, response) {
var twitterId = request.params.twitterId;
var authToken = request.params.authToken;
var authTokenSecret = request.params.authTokenSecret;
var url = "https://api.twitter.com/1.1/users/show.json";
Parse.Cloud.httpRequest({
url: url,
followRedirects: true,
headers: {
"Authorization": getOAuthSignature(url,authToken,authTokenSecret)
},
params: {
user_id: twitterId
}
}).then(...)
And lastly here's getOAuthSignature, the function used to sign the request (I took this from the example page in the github link):
var getOAuthSignature = function(url, authToken, authTokenSecret) {
var nonce = OAuth.nonce(32);
var ts = Math.floor(new Date().getTime() / 1000);
var timestamp = ts.toString();
var consumerKey = <MY-APP-CONSUMER-KEY>
var consumerSecret = <MY-APP-CONSUMER-SECRET>
var accessor = {
"consumerSecret": consumerSecret,
"tokenSecret": authTokenSecret
};
var params = {
"oauth_version": "1.0",
"oauth_consumer_key": consumerKey,
"oauth_token": authToken,
"oauth_timestamp": timestamp,
"oauth_nonce": nonce,
"oauth_signature_method": "HMAC-SHA1"
};
var message = {
"method": "GET",
"action": url,
"parameters": params
};
OAuth.SignatureMethod.sign(message, accessor);
var normPar = OAuth.SignatureMethod.normalizeParameters(message.parameters);
var baseString = OAuth.SignatureMethod.getBaseString(message);
var sig = OAuth.getParameter(message.parameters, "oauth_signature") + "=";
var encodedSig = OAuth.percentEncode(sig);
return 'OAuth oauth_consumer_key="'+consumerKey+'", oauth_nonce=' + nonce + ', oauth_signature=' + encodedSig + ', oauth_signature_method="HMAC-SHA1", oauth_timestamp=' + timestamp + ',oauth_token="'+authToken+'", oauth_version="1.0"'
};
What could be wrong? I've spent two days on the matter now and I don't know what to do anymore.

The issue here was that user_id="<user-id> has to be encoded in the request header as well as all the other oauth_* parameters. So I had to change this section:
var params = {
"user_id": twitterId, // <- add here
"oauth_version": "1.0",
"oauth_consumer_key": consumerKey,
"oauth_token": authToken,
"oauth_timestamp": timestamp,
"oauth_nonce": nonce,
"oauth_signature_method": "HMAC-SHA1"
};
And I'm passing the userId from the outer function, like getOAuthSignature(url,twitterId,authToken,authTokenSecret).
As for passing auth token data from device to cloud, this is probably not needed because you can find all the authentication info in the authData field of any ParseUser (as long as it is linked with twitter or Facebook).

Related

Message saying [Ljava.lang.Object] being delivered in place of values

I'm trying to collect tweets from Twitter API V2:
https://developer.twitter.com/en/docs/twitter-api/tweets/timelines/api-reference/get-users-id-tweets
The script I'm using to send the tweet value data to a Google Sheets cell is:
function TwitterTest() {
var string_Screen_name = "1310800524619386880";
var string_Consumer_key = "AAAAAAAAAAAAAAAAAAAAAAAAAA";
var string_Consumer_secret = "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
var tokenUrl = "https://api.twitter.com/oauth2/token";
var tokenCredential = Utilities.base64EncodeWebSafe(string_Consumer_key + ":" + string_Consumer_secret);
var tokenOptions = {
headers : {
Authorization: "Basic " + tokenCredential,
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
},
method: "post",
payload: "grant_type=client_credentials"
};
var responseToken = UrlFetchApp.fetch(tokenUrl, tokenOptions);
var parsedToken = JSON.parse(responseToken);
var token = parsedToken.access_token;
var apiUrl = "";
var responseApi = "";
var apiOptions = {
headers : {
Authorization: 'Bearer ' + token
},
"method" : "get"
};
var apiUrl = 'https://api.twitter.com/2/users/'+ string_Screen_name +'/tweets?expansions=attachments.poll_ids,attachments.media_keys,author_id,entities.mentions.username,geo.place_id,in_reply_to_user_id,referenced_tweets.id,referenced_tweets.id.author_id&tweet.fields=attachments,author_id,context_annotations,conversation_id,created_at,entities,geo,id,in_reply_to_user_id,lang,possibly_sensitive,public_metrics,referenced_tweets,reply_settings,source,text,withheld&user.fields=created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,withheld&place.fields=contained_within,country,country_code,full_name,geo,id,name,place_type&poll.fields=duration_minutes,end_datetime,id,options,voting_status&media.fields=duration_ms,height,media_key,preview_image_url,type,url,width,public_metrics,non_public_metrics,organic_metrics,promoted_metrics';
responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
var obj_data = JSON.parse(responseApi.getContentText());
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tweets").getRange("A3").setValue(obj_data.data);
}
The result obtained:
{attachments={media_keys=[Ljava.lang.Object;#1152e2a4}, entities={urls=[Ljava.lang.Object;#1373921e}, possibly_sensitive=false, conversation_id=1402411015724175370, public_metrics={like_count=1, reply_count=0, quote_count=0, retweet_count=0}, created_at=2021-06-08T23:43:36.000Z, source=Twitter Web App, id=1402411015724175370, text=ALERT: New high roller bet posted!
A parlay bet has been placed for $5,403.62 to win $6,241.18.
To view this bet or copy it https:// t.co /lrBXjHN0At https:// t.co /nBPMsgXI2g, author_id=1310800524619386880, lang=en, reply_settings=everyone}
Specifically in urls the result is:
urls=[Ljava.lang.Object;#1373921e}
The expected result in urls would look something like (example collected from Twitter API V2 website):
In your situation, is the following modification the result you expect?
From:
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tweets").getRange("A3").setValue(obj_data.data);
To:
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Tweets").getRange("A3").setValue(JSON.stringify(obj_data.data));

Google Classroom API 401 Error

I am trying to create a Google Classroom course using the Google Classroom API and a service account. I am currently experimenting using JavaScript and I have everything set up and working to get a list of course. I set up a JWT and request an authentication token which I receive.
{"access_token":"----ACCESS TOKEN HERE----------","token_type":"Bearer","expires_in":3600}
When I use this to retrieve a user's course list (via GET) there is no problem. I receive back a proper response with a list of courses which I then display in a table.
When I try to use the same process to try to create a course (via POST), I get a 401 error:
{
"error": {
"code": 401,
"message": "The request does not have valid authentication credentials.",
"status": "UNAUTHENTICATED"
}
}
This is the code I use to authenticate:
function authenticate(callback) {
function b64EncodeUnicode(str) {
str = JSON.stringify(str);
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function(match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
// constuct the JWT
var jwtHeader = {
"alg":"RS256",
"typ":"JWT"
}
jwtHeader = JSON.stringify(jwtHeader);
//construct the Claim
var jwtClaim = {
"iss":"psclassroomsync#psclassroomsync.iam.gserviceaccount.com",
"scope":"https://www.googleapis.com/auth/classroom.courses https://www.googleapis.com/auth/classroom.rosters",
"sub":"myemail#address.com", //this is an admin account I shouldn't really need this but tried with and without it
"aud":"https://www.googleapis.com/oauth2/v4/token",
"exp":(Math.round(new Date().getTime()/1000) + 60 * 10),
"iat":Math.round(new Date().getTime()/1000)
}
jwtClaim = JSON.stringify(jwtClaim);
//construct the signature
var key="-----BEGIN PRIVATE KEY-----Removed-----END PRIVATE KEY-----\n";
var jwtSign = b64EncodeUnicode(jwtSign);
var sJWT = KJUR.jws.JWS.sign("RS256", jwtHeader, jwtClaim, key);
var jwt = jwtHeader + "." + jwtClaim + "." + sJWT;
//request Token
var grantType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
var tokenRequest = "grant_type=" + grantType + "&assertion=" + sJWT;
var postURL = "https://www.googleapis.com/oauth2/v4/token"
request = $j.ajax({
url: postURL,
type: "post",
data: tokenRequest,
success: callback
});
}
This is the code I use to GET the course list. (this works)
$j("#getClasses").click(function(event){
function getClasses(callback){
authenticate(function(data){
console.log(JSON.stringify(data));
var access_token = data["access_token"];
var apiUrl = 'https://classroom.googleapis.com/v1/courses'
var myData = 'teacherId=~(teacheremail)&access_token='+access_token;
var files = $j.ajax({
url: apiUrl,
type: "get",
data: myData,
success: function (data) {
var retreivedClasses = JSON.stringify(data);
for(var i = 0; i < data['courses'].length; i++){
nextObject = data['courses'];
$j('#classListTable').append('<tr><td>' + nextObject[i]['name'] + '</td><td>' + nextObject[i]['courseState'] + '</td><td>' + nextObject[i]['enrollmentCode'] + '</td></tr>');
}
//$j('#classList').text(retreivedClasses);
}
});
});
}
getClasses();
});
This is the code that I use to create a course via POST. I've hard coded a few of the variables for testing but still gives the 401 error.
$j("#createClass").click(function(event){
function createClass(callback){
authenticate(function(data){
console.log(JSON.stringify(data));
var access_token = data["access_token"];
var tokenInfo = $j.ajax({
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
type: 'get',
data: "access_token="+access_token
});
var apiUrl = 'https://classroom.googleapis.com/v1/courses'
var myData = 'access_token='+access_token + '&ownerId=myemail#address.com&name=myClass'
console.log(myData);
var newGoogleClassroom = $j.ajax({
url: apiUrl,
type: "post",
data: myData,
success: function (data) {
var apiResponse = JSON.stringify(data);
$j('#classCreated').text(apiResponse);
}
});
});
};
createClass();
});
Finally, this is what I get when I get the token info. It looks fine to me i.e. proper scopes: (but I am new at this)
{
"azp": "removed",
"aud": "removed",
"scope": "https://www.googleapis.com/auth/classroom.courses https://www.googleapis.com/auth/classroom
.rosters",
"exp": "1474512198",
"expires_in": "3600",
"access_type": "offline"
}
I'd be grateful for any help.
Doug
P.S. I get the security implications of this code. It is in a secure environment for experimentation only. It won't see the light of day.
Based from this forum which is also receiving a 401 error, try to revoke the old oauth. As stated in this related thread, the 401 Unauthorized error you experienced may be related to OAuth 2.0 Authorization using the OAuth 2.0 client ID.
Suggested action: Refresh the access token using the long-lived refresh token. If this fails, direct through the OAuth flow.

Signing an OAuth request with an array parameter

I'm having a problem when signing an OAuth request that contains an array in the URL parameters.
Here's an example of a URL i want to sign:
http://example.com/oauth1/products?filter[categorie]=myCategorie&page=1
I'm using OAuthSignatureJS to generate the signature, and it works perfectly when passing simple parameters but when adding an array is returns an Invalid Signature response.
I tried using a JSON instead of url but still the same, and i can't change the API server code.
This is the calling function(AngularJS)
var request = generateRequest('GET', 'products', page, 'myCategorie');
request = request+'&page='+page+'&filter[categorie]=myCategorie';
$http.get(request, {timeout: config.timeout})
.success(function(json, status, headers, config){
})
.error(function(error){
console.log(error);
})
And this is the function i use to generate the request
var generateRequest = function(method, route, page = null, categorie = null) {
var timestamp = Math.floor(new Date().getTime()/1000);
var nonce = generateNonce();
var parameters = {
oauth_consumer_key: api.key,
oauth_nonce: nonce,
oauth_timestamp: timestamp,
oauth_signature_method: api.signature,
oauth_version: api.version
};
if(page) parameters.page = page;
if(categorie) {
parameters.filter = [];
parameters.filter['categorie'] = categorie;
console.log(parameters);
}
var link = 'http://example.com/oauth1/' + route;
// Generating the signature with OauthSignatureJS
var signature = oauthSignature.generate(method, link, parameters, api.secret);
var authentification = '?oauth_consumer_key='+api.key+'&oauth_signature_method='+api.signature+'&oauth_version='+api.version+'&oauth_timestamp='+timestamp+'&oauth_nonce='+nonce+'&oauth_signature='+signature;
return link+authentification;
}
When passing the filter[categorie] parameter in the URL, the server respond with a 401 Unauthorized message.
Invalid Signature - provided signature does not match
Is there something wrong with my code or is it the OauthSignature JS library ?
Thanks in advance.
I found an answer, i just needed to add the filter array as a string to the parameters object parameters['filter[categorie]'] = categorie;
So this is the new working code
var generateRequest = function(method, route, page = null, categorie = null) {
var timestamp = Math.floor(new Date().getTime()/1000);
var nonce = generateNonce();
var parameters = {
oauth_consumer_key: api.key,
oauth_nonce: nonce,
oauth_timestamp: timestamp,
oauth_signature_method: api.signature,
oauth_version: api.version
};
if(page) parameters.page = page;
if(categorie) parameters['filter[categorie]'] = categorie;
var link = 'http://example.com/oauth1/' + route;
// Generating the signature with OauthSignatureJS
var signature = oauthSignature.generate(method, link, parameters, api.secret);
var authentification = '?oauth_consumer_key='+api.key+'&oauth_signature_method='+api.signature+'&oauth_version='+api.version+'&oauth_timestamp='+timestamp+'&oauth_nonce='+nonce+'&oauth_signature='+signature;
return link+authentification;
}

How to get Twitter follower count for more than 100 usernames in Google Sheets?

I don't know my JavaScript. So I am using someone else's:
var CONSUMER_KEY = 'xxxx'; // Register your app with Twitter.
var CONSUMER_SECRET = 'xxxx'; // Register your app with Twitter.
function getTwitterUserFollowers(id) {
// Encode consumer key and secret
var tokenUrl = "https://api.twitter.com/oauth2/token";
var tokenCredential = Utilities.base64EncodeWebSafe(
CONSUMER_KEY + ":" + CONSUMER_SECRET);
// Obtain a bearer token with HTTP POST request
var tokenOptions = {
headers: {
Authorization: "Basic " + tokenCredential,
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
},
method: "post",
payload: "grant_type=client_credentials"
};
var responseToken = UrlFetchApp.fetch(tokenUrl, tokenOptions);
var parsedToken = JSON.parse(responseToken);
var token = parsedToken.access_token;
// Authenticate Twitter API requests with the bearer token
var apiUrl = 'https://api.twitter.com/1.1/users/show.json?screen_name='+id;
var apiOptions = {
headers: {
Authorization: 'Bearer ' + token
},
"method": "get"
};
var responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
var result = "";
if (responseApi.getResponseCode() == 200) {
// Parse the JSON encoded Twitter API response
var tweets = JSON.parse(responseApi.getContentText());
return tweets.followers_count
}
Logger.log(result);
}
source: http://sarahmarshall.io/post/70812214349/how-to-add-twitter-follower-counts-to-a-google
Sarah Marshall provides a pretty awesome how-to on using a Google Sheets script with the Twitter API to get follower counts for a list of Twitter user names. But it times out after about 100 user names:
Service invoked too many times for one day: urlfetch. (line 21, file "Code")
I'm wondering how I can get around the rate limit, or account for it, and return follower counts for more than 100 user names. Any ideas?
The function you are using does too many things: it both obtains a token and uses it to retrieve followers. So, if you are invoking it 100 times, you obtain a token 100 times (and notably, time out on the line obtaining it); but you need it once. You should store the token somewhere, e.g., in the spreadsheet itself since you already have your private data in the script associated with it. Example:
function getToken() {
// Obtain a bearer token with HTTP POST request
var tokenOptions = {
headers: {
Authorization: "Basic " + tokenCredential,
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
},
method: "post",
payload: "grant_type=client_credentials"
};
var responseToken = UrlFetchApp.fetch(tokenUrl, tokenOptions);
var parsedToken = JSON.parse(responseToken);
var token = parsedToken.access_token;
SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SheetWithToken').getRange('A1').setValue(token);
}
The last line stores the token string in cell A1 of the sheet named SheetWithToken. You'd invoke this function once, manually from the Script Editor.
The following function does the rest: it can be invoked as a custom function =getFollowers(A2) from the spreadsheet. Custom functions, like other spreadsheet functions, are re-evaluated only when the parameter changes. Thus, if the column with IDs (say, A) has a thousand of entries, you can paste the custom function gradually, for a handful at once.
function getFollowers(id) {
// Authenticate Twitter API requests with the bearer token
var token = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('SheetWithToken').getRange('A1').getValue();
var apiUrl = 'https://api.twitter.com/1.1/users/show.json?screen_name='+id;
var apiOptions = {
headers: {
Authorization: 'Bearer ' + token
},
"method": "get"
};
var responseApi = UrlFetchApp.fetch(apiUrl, apiOptions);
var result = "";
if (responseApi.getResponseCode() == 200) {
// Parse the JSON encoded Twitter API response
var tweets = JSON.parse(responseApi.getContentText());
return tweets.followers_count
}
}
You could even hardcode your access token into the second function, instead of fetching it from the spreadsheet. Twitter's tokens do not expire.
Another thing to consider is to recast the function getFollowers so that it accepts an array of IDs and loops through them, returning an array of follower counts. I don't think this would help, though: you would still have to worry about rate-limit on Twitter side, and at the same time be limited to 30 second execution time limit for custom functions.

Fitbit OAuth API request, invalid signature

I am trying to make an API request to fitbit, using the oauth debugger from fitbit (https://dev.fitbit.com/apps/oauthtutorialpage) i am trying to figure out what i am doing wrong. I have added comments to my code below to help you understand what i am trying to achieve. What i am quite sure of is that i am either signing my request wrong, or using the wrong data to sign it. This is echoed by the API response.
I know there are more fitbit api questions here on stackoverflow, however did not find my answer there.
Is there anyone with more experience in Oauth signatures that knows what i could be doing wrong? Or could help me find a different approach to this?
var request = require('request');
var crypto = require('crypto');
var params = {
'oauth_consumer_key' : 'key12345',
'oauth_nonce' : Math.random().toString(36).substring(3), //random string
'oauth_signature_method' : 'HMAC-SHA1',
'oauth_timestamp' : Date.now().toString().substring(0,10), //timestamp with the same length as in the tutorial
'oauth_version' : '1.0'
}
var oauth_consumer_secret = 'secret123';
var post_string = 'POST&https://api.fitbit.com/oauth/request_token';
for(var key in params){
post_string += '&' + key + '=' + params[key];
}
/*At this point we have made a post string that we have to hash with hmac-sha1
the post string looks like this:
POST&https://api.fitbit.com/oauth/request_token&oauth_consumer_key=key12345&oauth_nonce=az6r8cqlzyqfr&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1439147378&oauth_version=1.0
The post_string from the tutorial looks like this:
POST&%2Foauth%2Frequest_token&oauth_consumer_key%3D%26oauth_nonce%3D%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1439145944%26oauth_version%3D1.0
*/
var hmac = crypto.createHmac('sha1', oauth_consumer_secret + "&");
// The tutorial page shows me the signature was 'signed with secret&'. I have tried with and without the & at the end, but without luck.
hmac.setEncoding('base64'); //i'm not sure if this is correct
hmac.write(post_string);
hmac.end();
var hash = hmac.read();
//and finally adding the hash to the parameters.
params.oauth_signature = hash;
//now, making the request with an authorization header.
var header='';
for (var key in params){
if(header.length === 0){
header = ' OAuth ' + key + '="' + params[key] + '"';
}
else{
header += ', ' + key + '="' + params[key] + '"';
}
}
/*
At this point the header parameter looks like this
OAuth oauth_consumer_key="key12345", oauth_nonce="jnr97ppvjs2lnmi", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1439148049", oauth_version="1.0", oauth_signature="random_signature"
The tutorial tells me to use the headers:
OAuth oauth_consumer_key="key12345", oauth_nonce="jnr97ppvjs2lnmi", oauth_signature="different_signature", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1439145944", oauth_version="1.0"
*/
var headers ={
'Authorization' : header
}
var url="https://api.fitbit.com/oauth/request_token";
var requestTimeout = 5000;
var opts = {
url: url,
timeout: requestTimeout,
headers : headers
}
request(opts, function (err, res, body) {
if (err) {
console.dir(err);
return;
}
var statusCode = res.statusCode;
if(res.statusCode === 200){
console.log(body);
}
else{
console.log("http-error-code: " + res.statusCode);
console.log(body);
}
})
/*
The response:
http-error-code: 401
{"errors":[{"errorType":"oauth","fieldName":"oauth_signature","message":"Invalid signature: 9fXI85C7GvZqMyW1AK1EkOSWZCY="}],"success":false}
*/
To get access token and secret use Grant (you can test FitBit in the playground).
Once you have access token and secret use Purest to make subsequent request to the FitBit API.
Here is an example on how to get the user's profile:
var Purest = require('purest')
var fitbit = new Purest({provider:'fitbit',
key:'[CONSUMER_KEY]', secret:'[CONSUMER_SECRET]'})
fitbit.get('user/-/profile', {
oauth:{token:'[ACCESS_TOKEN]', secret:'[ACCESS_SECRET]'}
}, function (err, res, body) {})
Alternatively you can use request for that:
var request = require('request')
request.get('https://api.fitbit.com/1/user/-/profile.json', {
oauth:{
consumer_key:'..',
consumer_secret:'..',
token:'..',
token_secret:'..'
}
}, function (err, res, body) {})
In short - don't try to implement the web server OAuth flow by yourself - use Grant, then use either Purest or request, just keep in mind that you don't have to pass all of the OAuth parameters by yourself, just pass the credentials.

Categories