I am lost with using HMAC SHA256 for api authentication. This is my first time using it and I'm not sure what I am missing although I suspect it has to do with the timestamp. Can someone please help me identify what it is I am missing?
Everytime I try and make an API call I get a response stating
data: { success: false, error: 'Not logged in: Invalid signature' }
Here are the requirements for making the API call including the HMAC SHA256.
Here is the code I am using currently:
const axios = require('axios');
var forge = require('node-forge');
require('dotenv').config()
// get timestamp
var time = new Date().getTime();
// generate and return hash
function generateHash(plainText,secretKey)
{
var hmac = forge.hmac.create();
hmac.start('sha256', secretKey);
hmac.update(plainText);
var hashText = hmac.digest().toHex();
return hashText
}
// set axios config
var config = {
url:"https://ftx.us/api/wallet/all_balances",
method:"GET",
headers :{
"FTXUS-KEY":process.env.FTX_API_KEY,
"FTXUS-TS":time,
"FTXUS-SIGN":generateHash(`${new Date()}${"GET"}${"/wallet/all_balances"}`,process.env.FTX_API_SECRET)
}
}
axios(config)
.then(response => {
console.log(response.data)
}).catch(function (error) {
console.log(error);
})
I had to go through the same issue, so here goes my code.
import * as crypto from "crypto";
import fetch from "node-fetch";
// a function to call FTX (US)
async function callFtxAPIAsync(secrets, method, requestPath, body) {
const timestamp = Date.now();
const signaturePayload = timestamp + method.toUpperCase() + "/api" + requestPath + (method.toUpperCase() == "POST" ? JSON.stringify(body) : "");
const signature = crypto.createHmac('sha256', secrets.secret)
.update(signaturePayload)
.digest('hex');
const response = await fetch("https://ftx.us/api" + requestPath, {
method: method,
body: body != null ? JSON.stringify(body) : "",
headers: {
'FTXUS-KEY': secrets.key,
'FTXUS-TS': timestamp.toString(),
'FTXUS-SIGN': signature,
"Content-Type": "application/json",
"Accepts": "application/json"
}
});
return await response.json();
}
then call a post endpoint as for example:
let resultQuote = await callFtxAPIAsync(secrets, "post", "/otc/quotes",
{
"fromCoin": "USD",
"toCoin": "ETH",
"size": usd
});
or a get one:
let resultQuote = await callFtxAPIAsync(secrets, "get", "/otc/quotes/1234");
I hope it helps 😄
You need to add the full URL path, except the domain, in your case /api is missing. Try this:
"FTXUS-SIGN":generateHash(`${new Date()}${"GET"}${"/api/wallet/all_balances"}`,process.env.FTX_API_SECRET)
Related
Hello I'm trying to use the coinase api using axios to make request. I have set up the neccessary api authentication using SHA256 HMAC. I have been able to make some GET Request and got response. I have been trying to make a POST Request but i have been getting 401 status code.
const name = "Test BTC Address";
const body = {
name: name
}
var encHash = {
baseUrl: 'https://api.coinbase.com',
method: 'POST',
path: '/v2/accounts/bbc2e3f7-a851-50ab-b4b3-a0f2a700846f/addresses',
body: body,
scopes: "wallet:addresses:create"
};
const sign = generateHashKey(encHash, key.APISECRET);
console.log(sign);
const config = {
headers: {
'CB-ACCESS-SIGN': sign.signature,
'CB-ACCESS-TIMESTAMP': sign.timestamp,
'CB-ACCESS-KEY': key.APIKEY,
'CB-VERSION': '2021-10-15'
}
}
const url = `${encHash.baseUrl}${encHash.path}`
console.log(url);
var options = await axios.post(url, body, config);
return res.send({data: options.data})
} catch (error) {
// console.error(error);
return res.send({error})
} ```
I am currently writing to an API to try and get a token. I'm nearly there but fallen at the last hurdle..
const fs = require('fs');
const https = require('https');
const ConfigParams = JSON.parse(fs.readFileSync('Config.json', 'utf8'));
const jwt = require('jsonwebtoken');
const apikey = ConfigParams.client_id;
var privateKey = fs.readFileSync(**MY KEY**);
var tkn;
const jwtOptions = {
algorithm: 'RS512',
header: { kid: 'test-1' }
}
const jwtPayload = {
iss: apikey,
sub: apikey,
aud: **API TOKEN ENDPOINT**,
jti: '1',
exp: 300
}
jwt.sign(jwtPayload,
privateKey,
jwtOptions,
(err, token) => {
console.log(err);
//console.log(token);
tkn = token;
let = tokenPayload = {
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer/',
client_assertion: tkn
}
tokenAuthOptions = {
payload: tokenPayload,
host: **HOST**,
path: **PATH**,
method: 'POST',
}
https.request(
tokenAuthOptions,
resp => {
var body = '';
resp.on('data', function (chunk) {
body += chunk;
});
resp.on('end', function () {
console.log(body);
console.log(resp.statusCode);
});
}
).end();
}
)
the encoded token comes back fine for the first part, the https request though returns a problem.
the response I get back is grant_type is missing, so I know I have a formatting problem due to this x-www-form-urlencoded, but I can't figure out how to fix it.
here is what the website said:
You need to include the following data in the request body in
x-www-form-urlencoded format:
grant_type = client_credentials client_assertion_type =
urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion = <your signed JWT from step 4> Here's a complete
example, as a CURL command:
curl -X POST -H "content-type:application/x-www-form-urlencoded"
--data \ "grant_type=client_credentials\ &client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion="
END POINT
Ideally I want a solution using the https request, but if that's not possible I'm open to other solutions.
Any help is greatly appreciated.
Thanks,
Craig
Edit - I updated my code based on a suggestion to:
const params = new url.URLSearchParams({
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer/',
client_assertion: tkn
});
axios.post("URL", params.toString()).then(resp => {
console.log("response was : " + resp.status);
}).catch(err => {
console.log("there was an error: " + err);
})
But I'm still getting an error code 400, but now with less detail as to why. (error code 400 has multiple message failures)
Postman is the best.
Thank for #Anatoly for your support which helped to point me in the right direction. I had no luck so used postman for the first time, and found it had a code snippet section, with four different ways of achieving this using node.js.
The solution with Axion was:
const axios = require('axios').default;
const qs = require('qs');
var data = qs.stringify({
'grant_type': 'client_credentials',
'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
'client_assertion': tkn
});
var config = {
method: 'post',
url: '',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
data: data
};
axios(config)
.then(function (response) {
console.log(JSON.stringify(response.status));
})
.catch(function (error) {
console.log(error);
});
I believe the issue was that I was not passing the information into 'data:' in combination with the querystring problem. Using qs.stringify to format the object, then passing this into the data: key solved the problem.
I am able to GET data from Walmart Canada Marketplace API, using Nodejs, but I am unable to post a new item with their bulk feed endpoint. I get back a 404 'bad request'. I am not sure if the error is in the POST request or in the JSON. I am also not sure if Walmart Canada really accepts JSON files or just claims to.
Here is my code:
import { createSign, randomBytes } from 'crypto'
import fetch from 'node-fetch';
import { resolve } from 'url';
import fs from 'fs';
const method = 'post';
const BASE_URL = 'https://marketplace.walmartapis.com/'
const PK_HEADER = '\n-----BEGIN PRIVATE KEY-----\n'
const PK_FOOTER = '\n-----END PRIVATE KEY-----\n'
const WALMART_CONSUMER = "consumer";
const WALMART_CHANNEL = 'channel';
const WALMART_SECRET = 'secret';
function generateCorrelationId () {
return randomBytes(16).toString('hex')
}
function generateSignature (url, method, timestamp) {
const privateKey = `${PK_HEADER}${WALMART_SECRET}${PK_FOOTER}`
const stringToSign = WALMART_CONSUMER + '\n' +
url + '\n' +
method.toUpperCase() + '\n' +
timestamp + '\n'
const sign = createSign('RSA-SHA256')
sign.update(stringToSign)
return sign.sign(privateKey, 'base64')
}
const url = resolve(BASE_URL, "/v3/ca/feeds?feedType=item")
const timestamp = Date.now()
const signature = generateSignature(url, method, timestamp)
const headers = {
'WM_SVC.NAME': 'Walmart Marketplace',
'WM_CONSUMER.ID': WALMART_CONSUMER,
'WM_SEC.TIMESTAMP': timestamp,
'WM_SEC.AUTH_SIGNATURE': signature,
'WM_QOS.CORRELATION_ID': generateCorrelationId(),
'WM_CONSUMER.CHANNEL.TYPE': WALMART_CHANNEL,
'WM_TENANT_ID' : 'WALMART.CA',
'WM_LOCALE_ID' : 'en_CA',
'Accept': 'application/xml',
'Content-Type': 'multipart/form-data'
}
fetch(url, {
method: method,
headers: headers,
formData: {
'file': {
'value': fs.createReadStream('wm_new_item.json'),
'options': {
'filename': 'wm_new_item.json'
}
}
}
})
.then(response => console.log(response))
.catch(function (error) {
console.log(error)
});
and here is my json file:
{
"MPItemFeedHeader":{
"subset":"INTERNAL",
"mart":"WALMART_CA",
"sellingChannel":"marketplace",
"subCategory":"toys_other",
"locale":"en",
"version":"3.2",
"requestId": "1606485958",
"requestBatchId": "1606485958",
"processMode":"REPLACE"
},
"MPItem":[
{
"Orderable":{
"sku":"2663d9467e64a0a94ab0eeeccd9f37dd",
"productIdentifiers":{
"productIdType":"UPC",
"productId":"078257310609"
},
"productName":"abc",
"brand":"abc",
"price":1.00,
"fulfillmentLagTime":6,
"floorPrice":1.00,
"startDate":"2021-04-20T08:37:30Z",
"endDate":"2021-04-20T08:37:30Z",
"ProductIdUpdate":"No",
"SkuUpdate":"No",
"ShippingWeight":1
},
"Visible":{
"Toys":{
"shortDescription":"abc",
"mainImageUrl":"https://images-na.ssl-images-amazon.com/images/I/71X4z76HUTS._AC_SL1500_.jpg",
"productSecondaryImageURL":[
"https://images-na.ssl-images-amazon.com/images/I/71X4z76HUTS._AC_SL1500_.jpg",
"https://images-na.ssl-images-amazon.com/images/I/71X4z76HUTS._AC_SL1500_.jpg"
],
"msrp":1.00,
"variantGroupId":"abc",
"variantAttributeNames":[
"color"
],
"isPrimaryVariant":"Yes"
}
}
}
]
}
Thanks!
I figured it out with the help of this answer on another question.
You are better off referencing this than Walmarts official documentation
You need to submit a post request to the "https://marketplace.walmartapis.com/v3/feeds endpoint appending your type on the url ?feedType=[something]
You need to make sure that your "Content-Type" is "application/json" if you are sending them JSON.
You do not need to send it multipart like the documentation suggests, just send your entire JSON file as the body and it should work correctly.
Just trying to get my Coinbase balance. I have tried making a bunch of different API keys, keep getting the same error:
{
"errors": [{
"id": "authentication_error",
"message": "invalid api key"
}]
}
Im using Node.js through Netlify Lambda functions.
Here's my code:
import fetch from "node-fetch"
import crypto from "crypto"
const mykey = '<KEY>'
const mysecret = '<SECRET>'
exports.handler = async (event, context) => {
const url = `https://api.coinbase.com/v2/accounts`
var nonce = Math.floor(new Date().getTime() * 1e-3)
var my_hmac = crypto.createHmac('SHA256', nonce+'POST'+'v2/accounts', mysecret)
my_hmac.update(nonce + url)
var signature = my_hmac.digest('hex')
var msg;
return fetch(url, { headers:
{
'CB-ACCESS-KEY' : mykey,
'CB-ACCESS-SIGN': signature,
'CB-ACCESS-TIMESTAMP': nonce,
'Content-Type': 'application/json'
}
}).then(res => {
// console.log(res)
res.json
})
.then(data => {
return ({
statusCode: 200,
body: JSON.stringify(data)
})
})
}
You are using the wrong names for the tokens.
ACCESS_KEY is supposed to be CB-ACCESS-KEY
ACCESS_SIGNATURE is supposed to be CB-ACCESS-SIGN
I couldn't find info about the nonce. I found this over here.
Update:
signature looks like it is not made properly:
The nonce+'POST'+'/v2/accounts' is supposed to be the value in my_hmac.update
In turn for the createHmac it is only supposed to be SHA256 and mysecret
The signature pre-hash value is supposed to have a / at the beginning
A useful reference is
here (be sure to click node.js at the top).
Got a 403 signature error , when using the below fetch function:
function elasticsearchFetch(AWS, elasticsearchDomain, endpointPath, options = {}, region = process.env.AWS_REGION) {
return new Promise((resolve, reject) => {
const { body, method = 'GET' } = options;
const endpoint = new AWS.Endpoint(elasticsearchDomain);
const request = new AWS.HttpRequest(endpoint, region);
request.method = method;
request.path += endpointPath;
request.headers.host = elasticsearchDomain;
if (body) {
request.body = body;
request.headers['Content-Type'] = 'application/json';
request.headers['Content-Length'] = request.body.length;
}
const credentials = new AWS.EnvironmentCredentials('AWS');
const signer = new AWS.Signers.V4(request, 'es');
signer.addAuthorization(credentials, new Date());
const client = new AWS.HttpClient();
client.handleRequest(request, null, (res) => {
let chunks = '';
res.on('data', (chunk) => {
chunks += chunk;
});
res.on('end', () => {
if (res.statusCode !== 201) console.log('Got these options STATUSCODE', JSON.stringify(options, false, 2));
return resolve({ statusCode: res.statusCode, body: chunks });
});
}, (error) => {
console.log('Got these options ERROR', JSON.stringify(options, false, 2));
return reject(error);
});
});
}
This is the options used for the request in above function :
{
"method": "POST",
"body": "{\"prefix\":\"image_233/ArtService/articles-0/GB/ART-60297885/\",\"id\":\"ART-60297885\",\"retailUnit\":\"GB\",\"commercial\":{\"name\":{\"en-GB\":\"FÖRBÄTTRA\"}},\"schemaType\":\"product\",\"productType\":\"ART\"}"
}
and got this error :
{
"statusCode": 403,
"body": "{\"message\":\"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\"}"
}
This is the endpoint : 233/_doc/
I believe your Content-Length header is incorrect, causing the signature mismatch.
Your payload includes the string FÖRBÄTTRA, which has two double-byte characters.
You're setting the Content-Length to request.body.length, which comes to 186.
While this is the number of characters in the body, it is not the number of bytes in the body (188).
To calculate the Content-Length, use Buffer.byteLength(request.body). For a POST request like this, you can even remove that line of code altogether, and the request will succeed.
// Content-Length is only needed for DELETE requests that include a request
// body, but including it for all requests doesn't seem to hurt anything.
request.headers['Content-Length'] = Buffer.byteLength(request.body);
Source: https://docs.aws.amazon.com/elasticsearch-service/latest/developerguide/es-request-signing.html#es-request-signing-node
By the way, why not use elasticsearch client for nodejs to communicate with elasticsearch rather than writing your own logic. You can consider using http-aws-es which does the request signing part for you. The code will look like
const { Client } = require("elasticsearch");
const esConnectionClass = require("http-aws-es");
const elasticsearchConfig = {
host: "somePath",
connectionClass: esConnectionClass
};
const nativeClient = new Client(elasticsearchConfig);
const result = await nativeClient.search({});