How can I do multipart/form-data request with hash key? - node.js

There are how I generate options for the request to the server:
function multipartFormData(params, url, key, nonce, signature) {
var boundary = '----WebKitFormBoundary' + nonce;
var bodyString = [];
bodyString.push(
'--' + boundary,
'Content-Disposition: form-data; name="document"; filename="image.png"',
'Content-Type: image/png',
'',
fs.createReadStream('image/image.png')
);
bodyString.push('--' + boundary + '--','');
var content = bodyString.join('\r\n');
return {
formData: content,
url: url + '/',
headers: {
'Content-Type': 'multipart/form-data; boundary=' + boundary,
'Apiauth-Key': key,
'Apiauth-Nonce': nonce,
'Apiauth-Signature': signature,
'Content-Length': content.length
}
}
}
There are the signature hash authentication key:
function getMessageSignature(path, params, nonce) {
var data_params = params;
var boundary = '----WebKitFormBoundary' + nonce;
var postParameters = querystring.stringify(data_params);
var path = '/api' + path + '/';
let message = nonce + config.key + path + 'POSTmultipart/form-data; boundary=' + boundary + 'image.png';
var auth_hash = crypto.createHmac("sha256", config.secret).update(message).digest('hex').toUpperCase();
return auth_hash;
}
So then I just do my request:
function Client(key, secret) {
var nonce = new Date() * 1000;
var config = {
url: 'https://somesite/api',
key: key,
secret: secret
};
// there is other all functions, 2 of them i have already written on top
var signature = getMessageSignature(path, params, nonce);
var options = multipartFormData(params, config.url, config.key, nonce, signature);
request.post(options, function(error, response, body) {
...
}
}
I am getting this error in the request body.error :
message: 'HMAC authentication key and signature was given, but they
are invalid

Related

OAuth 1.0 transform Consumer Secret into an oauth_signature

I am trying to implement Twitter login with my MERN application. Following twitter tutorials, i understand that all request should be signed with an OAuth headers. If i use postman, i enter my credentials (Consumer Key, Consumer Secret) in the authorization tab and the call works. The things is that postman transform the Consumer secret into and oauth_signature before sending the call. Now i want to do this workflow in Nodejs. All tutorials online use complicated passport strategies and the use of request module which is depricated. I understand that to generate oauth_signature one would have to generate an oauth_nonce and then do:
Percent encode every key and value that will be signed.
Sort the list of parameters alphabetically [1] by encoded key [2].
For each key/value pair:
Append the encoded key to the output string.
Append the ‘=’ character to the output string.
Append the encoded value to the output string.
If there are more key/value pairs remaining, append a ‘&’ character to the output string.
I am sure doing all this would be reinventing the wheel and am pretty sure there is a module that does this step specifically without all the passport and authentication (which is already done in my app) i simply need to sign my twitter requests like Postman does nothing more.
I tried the following but it seems am still doing something wrong
var axios = require('axios');
const jsSHA = require('jssha/sha1');
//Am i using the right library??
const callBackUL = 'https%3A%2F%2F127.0.0.1%3A3000%2Flogin';
var oauth_timestamp = Math.round(new Date().getTime() / 1000.0);
const nonceObj = new jsSHA('SHA-1', 'TEXT', { encoding: 'UTF8' });
nonceObj.update(Math.round(new Date().getTime() / 1000.0));
const oauth_nonce = nonceObj.getHash('HEX');
const endpoint = 'https://api.twitter.com/oauth/request_token';
const oauth_consumer_key = process.env.TWITTER_API_KEY;
const oauth_consumer_secret = process.env.TWITTER_API_SECRET;
var requiredParameters = {
oauth_consumer_key,
oauth_nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp,
oauth_version: '1.0'
};
const sortString = requiredParameters => {
var base_signature_string = 'POST&' + encodeURIComponent(endpoint) + '&';
var requiredParameterKeys = Object.keys(requiredParameters);
for (var i = 0; i < requiredParameterKeys.length; i++) {
if (i == requiredParameterKeys.length - 1) {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]]
);
} else {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]] +
'&'
);
}
}
return base_signature_string;
};
const sorted_string = sortString(requiredParameters);
console.log('Sorted string:', sorted_string);
const signing = (signature_string, consumer_secret) => {
let hmac;
if (
typeof signature_string !== 'undefined' &&
signature_string.length > 0
) {
//console.log('String OK');
if (
typeof consumer_secret !== 'undefined' &&
consumer_secret.length > 0
) {
// console.log('Secret Ok');
const secret = consumer_secret + '&';
var shaObj = new jsSHA('SHA-1', 'TEXT', {
hmacKey: { value: secret, format: 'TEXT' }
});
shaObj.update(signature_string);
hmac = encodeURIComponent(shaObj.getHash('B64'));
//var hmac_sha1 = encodeURIComponent(hmac);
}
}
return hmac;
};
const signed = signing(sorted_string, oauth_consumer_secret);
console.log(signed);
var data = {};
var config = {
method: 'post',
url: endpoint,
headers: {
Authorization: `OAuth oauth_consumer_key=${process.env.TWITTER_API_KEY},oauth_signature_method="HMAC-SHA1",oauth_timestamp=${oauth_timestamp},oauth_nonce=${oauth_nonce},oauth_version="1.0",oauth_callback=${callBackUL},oauth_consumer_secret=${signed}`,
'Content-Type': 'application/json'
},
data: data
};
try {
const response = await axios(config);
console.log(JSON.stringify(response.data));
} catch (err) {
console.log(err.response.data);
}
next();
});
SOLVED
var axios = require('axios');
const jsSHA = require('jssha/sha1');
const callBackUL = 'https%3A%2F%2F127.0.0.1%3A3000%2Flogin';
var oauth_timestamp = Math.round(new Date().getTime() / 1000.0);
const nonceObj = new jsSHA('SHA-1', 'TEXT', { encoding: 'UTF8' });
nonceObj.update(Math.round(new Date().getTime() / 1000.0));
const oauth_nonce = nonceObj.getHash('HEX');
const endpoint = 'https://api.twitter.com/oauth/request_token';
const oauth_consumer_key = process.env.TWITTER_API_KEY;
const oauth_consumer_secret = process.env.TWITTER_API_SECRET;
var requiredParameters = {
oauth_callback: callBackUL,
oauth_consumer_key,
oauth_nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp,
oauth_version: '1.0'
};
const sortString = requiredParameters => {
var base_signature_string = 'POST&' + encodeURIComponent(endpoint) + '&';
var requiredParameterKeys = Object.keys(requiredParameters);
for (var i = 0; i < requiredParameterKeys.length; i++) {
if (i == requiredParameterKeys.length - 1) {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]]
);
} else {
base_signature_string += encodeURIComponent(
requiredParameterKeys[i] +
'=' +
requiredParameters[requiredParameterKeys[i]] +
'&'
);
}
}
return base_signature_string;
};
const sorted_string = sortString(requiredParameters);
console.log('Sorted string:', sorted_string);
const signing = (signature_string, consumer_secret) => {
let hmac;
if (
typeof signature_string !== 'undefined' &&
signature_string.length > 0
) {
//console.log('String OK');
if (
typeof consumer_secret !== 'undefined' &&
consumer_secret.length > 0
) {
// console.log('Secret Ok');
const secret = encodeURIComponent(consumer_secret) + '&';
var shaObj = new jsSHA('SHA-1', 'TEXT', {
hmacKey: { value: secret, format: 'TEXT' }
});
shaObj.update(signature_string);
hmac = encodeURIComponent(shaObj.getHash('B64'));
}
}
return hmac;
};
const signed = signing(sorted_string, oauth_consumer_secret);
console.log(signed);
var data = {};
var config = {
method: 'post',
url: endpoint,
headers: {
Authorization: `OAuth oauth_consumer_key=${process.env.TWITTER_API_KEY},oauth_nonce=${oauth_nonce},oauth_signature=${signed},oauth_signature_method="HMAC-SHA1",oauth_timestamp=${oauth_timestamp},oauth_version="1.0",oauth_callback=${callBackUL}`,
'Content-Type': 'application/json'
},
data: data
};
try {
const response = await axios(config);
console.log(JSON.stringify(response.data));
} catch (err) {
console.log(err.response.data);
}
next();

I am getting an authentication error while updating data in Azure table storage using SharedKeyLite in NodeJS code

I am trying to update Azure Table storage using Rest API and SharedKeyLite in my nodejs code but getting authentication error -
"Server failed to authenticate the request. Make sure the value of
Authorization header is formed correctly including the signature."
Please find my nodejs code below-
const Request = require('request');
const CryptoJS = require('crypto-js');
let apiVersion = '2018-03-28';
let contentType = 'application/json';
let dataServiceVersion = '3.0;NetFx';
let maxDataServiceVersion = '3.0;NetFx';
let storageAccountName = "mybotstorage";
let tableName = "myBotCounter";
let key = "**********************YO+MnTwBBbKKcPfsFQwg==";
var strTime = (new Date()).toUTCString();
let strToSign = strTime + '\n/' + storageAccountName + '/' + tableName + '(PartitionKey=\'mypartkey\',RowKey=\'myrowkey\')';
var secret = CryptoJS.enc.Base64.parse(key);
var hash = CryptoJS.HmacSHA256(strToSign, secret);
var hashInBase64 = CryptoJS.enc.Base64.stringify(hash);
var auth = "SharedKeyLite mybotstorage:" + hashInBase64;
var postData = {
"counter": "1050"
}
let content = Buffer.from(JSON.stringify(postData));
let contentLength = content.byteLength;
let headers = {};
headers['Authorization'] = auth;
headers['x-ms-date'] = strTime;
headers['x-ms-version'] = apiVersion;
headers['DataServiceVersion'] = dataServiceVersion;
headers['MaxDataServiceVersion'] = maxDataServiceVersion;
headers['Content-Type'] = contentType;
headers["Content-Length"] = contentLength;
headers["Accept-Charset"] = "UTF-8";
headers["Accept"] = "application/json";
let url = "https://mybotstorage.table.core.windows.net/" + tableName + "(PartitionKey='mypartky',RowKey='myrowkey')";
var options = {
headers: headers,
method: 'PUT',
body: content,
url: url
}
Request(options, function (err, res, body) {
if (err) {
console.error('error posting json: ', err)
throw err
}
var headers = res.headers;
var statusCode = res.statusCode;
console.log('headers: ', headers);
console.log('statusCode: ', statusCode);
console.log('body: ', body);
});
Note : Retrieving data with 'GET' and inserting data with 'POST' works fine using nodejs, only I am not able to update data with 'PUT' method in nodejs.
Also I tested 'PUT' method using C# code, it works fine but nodejs code gives an authentication error.
The issue was due to encoding of url which is handled differently by C# and NodeJS.
I am posting the working code sample for reference.
const Request = require('request');
const CryptoJS = require('crypto-js');
let apiVersion = '2013-08-15';
let contentType = 'application/json';
let storageAccountName = "mybotstorage";
let tableName = "myBotCounter";
let key = "*******************************YO+MnTwBBbKKcPfsFQwg==";
let strTime = (new Date()).toUTCString();
let strToSign = strTime + '\n/' + storageAccountName + '/' + tableName + '(PartitionKey=%27mypartitionkey%27,RowKey=%27myrowkey%27)';
let secret = CryptoJS.enc.Base64.parse(key);
let hash = CryptoJS.HmacSHA256(strToSign, secret);
let hashInBase64 = CryptoJS.enc.Base64.stringify(hash);
let auth = "SharedKeyLite mybotstorage:" + hashInBase64;
console.log(strToSign);
console.log(auth);
let postData = {
"counter": "1088"
}
let headers = {};
headers['Content-Type'] = contentType;
headers['Authorization'] = auth;
headers['x-ms-date'] = strTime;
headers['x-ms-version'] = apiVersion;
let url = "https://mybotstorage.table.core.windows.net/" + tableName + "(PartitionKey=%27mypartitionkey%27,RowKey=%27myrowkey%27)";
console.log('url------', url);
var options = {
headers: headers,
method: 'PUT',
body: JSON.stringify(postData),
url: url
}
Request(options, function (err, res, body) {
if (err) {
console.error('error posting json: ', err)
throw err
}
var headers = res.headers;
var statusCode = res.statusCode;
console.log('headers: ', headers);
console.log('statusCode: ', statusCode);
console.log('body: ', body);
});

how do I construct a signature for a delete request which delete an entity in azure datastore in nodejs REST implementation?

I am implementing REST-API for deleting key/entity from table in azure datastore, but seems it has issue in authorization token, and i think there is issue in making 'stringTosign' i.e. 'Signature' in azure.
Below are code,
const key = {
"Namespace": "my-app",
"Path": [
{
"Kind": 'tblxyz',
"Name": "test-datastore-row"
}
]
};
REST Implementation:
public async DeleteAPI(key: Key): Promise<any> {
const tableService = await this.GetTableServiceAsync(); // gives me table metadata
const url = "https://" + tableService.storageAccount + ".table.core.windows.net/" + 'tblxyz' + '(PartitionKey=' + '\'' + key.Namespace + '\'' + ',' + 'RowKey=' + '\'' + key.Path[0].Name + '\'' + ')';
const timestamp = (new Date()).toUTCString();
const stringToSign = timestamp + '\n/' + tableService.storageAccount + '/' + 'tblxyz';
const hmac = crypto.createHmac('sha256', new Buffer(tableService.storageAccessKey, 'base64'))
.update(stringToSign, 'utf-8')
.digest('base64');
return new Promise((resolve, reject) => {
request.delete({
'headers': {
'Authorization': 'SharedKeyLite ' + tableService.storageAccount + ':' + hmac,
'x-ms-date': timestamp,
'x-ms-version': '2016-05-31',
'Content-Type': 'application/json',
'If-Match': '*'
},
'url': url,
'json': true
}, function (err, result) {
if (err) {
console.log('inside delete err', JSON.stringify(err));
return reject(err);
}
if (result.statusCode !== 200 && result.statusCode !== 204) {
console.log('inside delete err: 2', JSON.stringify(result));
return reject(result.body);
}
return resolve();
});
});
}
while making call to this API, i am getting below error,
Anybody have face this problem? Need help ...!!!!
Error message:
Error: the object {
"odata.error": {
"code": "AuthenticationFailed"
"message": {
"lang": "en-US"
"value": "Server failed to authenticate the request. Make sure the value
of Authorization header is formed correctly including the
signature.\nRequestId:df933tt7-0002-0004-41c3-782e5g000000\nTime:2017-12-
19T12:16:37.5434074Z"
}
}
} was thrown, throw an Error :)
at <anonymous>
at process._tickDomainCallback (internal/process/next_tick.js:228:7)
It seems there's an issue with your stringToSign. Based on the documentation, it should include resource's encoded URI path. Can you try with the following:
const stringToSign = timestamp + '\n/' + tableService.storageAccount + '/' + 'tblxyz' + '(PartitionKey=' + '\'' + key.Namespace + '\'' + ',' + 'RowKey=' + '\'' + key.Path[0].Name + '\'' + ')';
UPDATE
Please see this sample code. Essentially the resource's path should be URL encoded:
const request = require("request");
const crypto = require("crypto");
const url = require('url');
var accountName = "account-name";
var accountKey = "account-key";
var tableName = "table-name";
var pk = "partition-key-value";
var rk = "row-key-value";
const encodedUriPath = tableName + '(PartitionKey=' + '\'' + pk + '\'' + ', ' + 'RowKey=' + '\'' + rk + '\'' + ')';
const endpoint = "https://" + accountName + ".table.core.windows.net/" + encodedUriPath;
const parsedUrl = url.parse(endpoint);
const timestamp = (new Date()).toUTCString();
console.log(url);
console.log(timestamp);
const stringToSign = timestamp + '\n/' + accountName + parsedUrl.path;
console.log('--------------------------------------');
console.log(stringToSign);
const hmac = crypto.createHmac('sha256', new Buffer(accountKey, 'base64'))
.update(stringToSign, 'utf-8')
.digest('base64');
console.log('--------------------------------------');
console.log(hmac);
request.delete({
'headers': {
'Authorization': 'SharedKeyLite ' + accountName + ':' + hmac,
'x-ms-date': timestamp,
'x-ms-version': '2016-05-31',
'Content-Type': 'application/json',
'If-Match': '*'
},
'url': endpoint,
'json': true
}, function (err, result) {
if (err) {
console.log('inside delete err', JSON.stringify(err));
} else {
console.log(JSON.stringify(result));
}
});
FYI Azure storage Node.js client library can help to fulfill such work with sample:
var azure = require('azure-storage');
var tableService = azure.createTableService();
var deleteEntity = {
PartitionKey: {'_': 'partitionKey'},
RowKey: {'_': 'rowKey'}
};
tableService.deleteEntity('mytable', deleteEntity, function(error, result, response) {
if(error) {
// Delete table entity error
} else {
// Delete table entity successfully
}
});
Would you share your concern about directly using azure-storage package?
Best Wishes

NodeJs HTTP proxy basic auth

I am trying to implement a simple HTTP proxy that will only try to perform basic auth on the target host.
So far I have the following:
var http = require('http');
const my_proxy = http.createServer(function(request, response) {
console.log(request.connection.remoteAddress + ": " + request.method + " " + request.url);
const options = {
port: 80
, host: request.headers['host']
, method: request.method
, path: request.url
, headers: request.headers
, auth : 'real_user:real_password'
}
};
var proxy_request = http.request(options);
proxy_request.on('response', function (proxy_response) {
proxy_response.on('data', function(chunk) {
response.write(chunk, 'binary');
});
proxy_response.on('end', function() {
response.end();
});
response.writeHead(proxy_response.statusCode, proxy_response.headers);
});
request.on('data', function(chunk) {
proxy_request.write(chunk, 'binary');
});
request.on('end', function() {
proxy_request.end();
});
});
my_proxy.listen(8080);
However, "auth : 'real_user:real_password'" doesn't seem to do anything. Also have tried:
...
auth: {
user: real_user,
pass: real_pass
}
...
You have to generate the auth header
var username = 'Test';
var password = '123';
var auth = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
// auth is: 'Basic VGVzdDoxMjM='
var header = {'Host': 'www.example.com', 'Authorization': auth};
var request = client.request('GET', '/', header);
DeprecationWarning: Buffer() is deprecated due to security and usability issues. Please use the
var username = 'Test';
var password = '123';
// Deprecated
// var auth = 'Basic ' + new Buffer(username + ':' + password).toString('base64');
var auth = 'Basic ' + Buffer.from(username + ':' + password).toString('base64');
// auth is: 'Basic VGVzdDoxMjM='
var header = {'Host': 'www.example.com', 'Authorization': auth};
var request = client.request('GET', '/', header);

Node.js YouTube API Upload unable to convert video

I'm trying to upload video to youtube programatically. I chose to use Node.js for the task.
I get an XML response as well as an HTTP Status Code of 201 and I see the video appear in video manager, however the video always has the message "Failed (unable to convert video file)".
I can upload the file through YouTube's own uploader on their page and there are no problems. I only have to upload to a single account, so I set up the OAuth2 for the account and stored the refresh token. The refresh token is hard-coded, though I replace it with a variable below.
Does the refresh token need to, itself, be refreshed?
My code:
var qs = require('querystring'),
https = require('https'),
fs = require('fs');
var p_data = qs.stringify({
client_id: myClientID,
client_secret: myClientSecret,
refresh_token: refreshTokenForAccount,
grant_type: 'refresh_token'
});
var p_options = {
host: 'accounts.google.com',
port: '443',
method: 'POST',
path: '/o/oauth2/token',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': p_data.length,
'X-GData-Key': myDeveloperKey
}
};
var file_path = process.argv[1] || "video.mp4";
var json = "";
var p_req = https.request(p_options, function(resp){
resp.setEncoding( 'utf8' );
resp.on('data', function( chunk ){
json += chunk;
});
resp.on("end", function(){
debugger;
var access_token = JSON.parse(json).access_token;
var title="test upload1",
description="Second attempt at an API video upload",
keywords="",
category="Comedy";
var file_reader = fs.createReadStream(file_path, {encoding: 'binary'});
var file_contents = '';
file_reader.on('data', function(data)
{
file_contents += data;
});
file_reader.on('end', function()
{
var xml =
'<?xml version="1.0"?>' +
'<entry xmlns="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:yt="http://gdata.youtube.com/schemas/2007">' +
' <media:group>' +
' <media:title type="plain">' + title + '</media:title>' +
' <media:description type="plain">' + description + '</media:description>' +
' <media:category scheme="http://gdata.youtube.com/schemas/2007/categories.cat">' + category + '</media:category>' +
' <media:keywords>' + keywords + '</media:keywords>' +
' </media:group>' +
'</entry>';
var boundary = Math.random();
var post_data = [];
var part = '';
part = "--" + boundary + "\r\nContent-Type: application/atom+xml; charset=UTF-8\r\n\r\n" + xml + "\r\n";
post_data.push(new Buffer(part, "utf8"));
part = "--" + boundary + "\r\nContent-Type: video/mp4\r\nContent-Transfer-Encoding: binary\r\n\r\n";
post_data.push(new Buffer(part, 'utf8'));
post_data.push(new Buffer(file_contents, 'binary'));
post_data.push(new Buffer("\r\n--" + boundary + "--\r\n\r\n", 'utf8'));
var post_length = 0;
for(var i = 0; i < post_data.length; i++)
{
post_length += post_data[i].length;
}
var options = {
host: 'uploads.gdata.youtube.com',
port: 443,
path: '/feeds/api/users/default/uploads',
method: 'POST',
headers: {
'Authorization': 'Bearer ' + access_token,
'X-GData-Key': myDeveloperKey,
'Slug': 'video.mp4',
'Content-Type': 'multipart/related; boundary="' + boundary + '"',
'Content-Length': post_length,
'Connection': 'close'
}
}
var req = https.request(options, function(res)
{
res.setEncoding('utf8');
console.dir(res.statusCode);
console.dir(res.headers);
var response = '';
res.on('data', function(chunk)
{
response += chunk;
});
res.on('end', function()
{
console.log( "We got response: " );
console.log(response);
});
});
for (var i = 0; i < post_data.length; i++)
{
req.write(post_data[i]);
}
req.on('error', function(e) {
console.error(e);
});
req.end();
});
});
});
p_req.write(p_data);
p_req.end();
The problem was in the file being uploaded.
This line: var file_path = process.argv[1] || "video.mp4"; should have been var file_path = process.argv[2] || "video.mp4";
Note argv[1] is the absolute path to the script being run, argv[2] is the first command line argument passed to the script.
Of course YouTube would fail to convert the "video", it wasn't video at all it was the script being run.

Resources