Reputation: 1425
I'm trying to hook into eTrade's API for a project. They use an OAuth v1 flow. I'm running into error's on the very first step of the flow- getting the request token. The info from their doc's can be found here.
The error I'm getting is 401- invalid signature.
I've been reading up on the OAuth flow here and have become familiar with the process. In the past I've used PassportJS for 3rd party integrations.
So eTrade has provided me 4 keys- PROD_KEY, PROD_SECRET, SANDBOX_KEY, SANDBOX_SECRET. I'm using the sandbox keys for now as my production key is still pending.
Now I have an endpoint on my API that I'll call from my client to get the request tokens. It's structure like this:
router.route('/auth/request').get(controller.request);
And the following controller is where I'm generating a signature and trying to request the token from eTrade. That controller looks like this:
// controllers/auth.js
const axios = require('axios');
const { v1 } = require('uuid');
const crypto = require('crypto');
function generateSignature() {
const method = 'GET',
url = 'http://localhost/',
encodedUrl = encodeURIComponent(url),
paramaters = {
oauth_consumer_key: process.env.ETRADE_SANDBOX_API_KEY,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: Math.floor(Date.now() / 1000),
oauth_nonce: v1(),
oauth_callback: 'oob'
}
var ordered = {};
Object.keys(paramaters).sort().forEach((key) => {
ordered[key] = paramaters[key];
});
var encodedParameters = '';
for(k in ordered) {
const encodedValue = escape(ordered[k]);
const encodedKey = encodeURIComponent(k);
if(encodedParameters === '') {
encodedParameters += encodeURIComponent(`${encodedKey}=${encodedValue}`);
} else {
encodedParameters += encodeURIComponent(`&${encodedKey}=${encodedValue}`);
}
}
encodedParameters = encodeURIComponent(encodedParameters);
const signature_base_string = `${method}&${encodedUrl}&${encodedParameters}`;
const signing_key = `${process.env.ETRADE_SANDBOX_SECRET_KEY}`;
const signature = crypto.createHmac("SHA1", signing_key).update(signature_base_string).digest().toString('base64');
const encodedSignature = encodeURIComponent(signature);
return encodedSignature;
}
module.exports = {
request: async (req, res) => {
console.log('Fetching request token from etrade...');
try {
var response = await axios.get(`https://api.etrade.com/oauth/request_token`, {
params: {
oauth_consumer_key: process.env.ETRADE_SANDBOX_API_KEY,
oauth_signature_method: 'HMAC-SHA1',
oauth_signature: generateSignature(),
oauth_timestamp: Math.floor(Date.now() / 1000),
oauth_nonce: v1(),
oauth_callback: 'oob'
}
});
console.log('Fetched request token...', response.data);
} catch (error) {
console.log('Could not fetch request token...', error.response.data);
}
}
}
I have followed some guides on the internet to help me understand the process of signing a request, but I can't seem to get a valid response back.
Upvotes: 1
Views: 920
Reputation: 88
I don't know if you are still looking for an answer on this, but I think the following might help. I ran into a similar issue while I was trying to integrate with their API (I am using java though). Looking into their sample code, I realized that the keys to sign the baseString requires an extra '&' at the end for request_token API. See the following code from their sample application -
if( token != null){
// not applicable for request_token API call
key = StringUtils.isEmpty(token.getOauth_token_secret()) ? context.getResouces().getSharedSecret() +"&" :
context.getResouces().getSharedSecret()+"&" + OAuth1Template.encode(token.getOauth_token_secret());
}else{
// applicable for request_token API call
key = context.getResouces().getSharedSecret() +"&";
}
AFAIK, this is not oAuth standard or anything. This seems to be very specific to their implementation. Anyways, I updated my code to include the extra '&' at the end & it started working.
private String signBaseString(String httpMethod, String url) {
StringBuilder baseString = new StringBuilder();
baseString.append(encode(httpMethod)).append("&").append(encode(url)).append("&");
baseString.append(encode(normalizeParams(url)));
// the extra & is added here.
SecretKeySpec signingKey = new SecretKeySpec((this.apiSecret + "&").getBytes(), SIGN_ALG);
Mac mac = null;
try {
mac = Mac.getInstance(SIGN_ALG);
mac.init(signingKey);
return new String(Base64.encodeBase64(
mac.doFinal(baseString.toString().getBytes(
StandardCharsets.UTF_8))));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
logger.error("Error during signing headers", e);
return null;
}
}
Let me know if this works for you.
Upvotes: 1
Reputation: 853
Your code is targeting the etrade production URL. Try calling the sandbox URL.
Production https://api.etrade.com/v1/{module}/{endpoint}
Sandbox https://apisb.etrade.com/v1/{module}/{endpoint}
Upvotes: 1