I'm trying to publish an app on Shopify marketplace by following this documentation. And I'm stuck on step-3 of the oauth documentation wherein you have to do 'HMAC Signature Validation'.
Documentation states that you have to process the string (specified below) through HMAC-SHA256 using app's shared secret key.
String = "shop=some-shop.myshopify.com×tamp=1337178173"
I'm trying to implement the steps using Java. Following is gist of the code that I have used.
private static final String HMAC_ALGORITHM = "HmacSHA256";
String key = "hush";
String data = "shop=some-shop.myshopify.com×tamp=1337178173";
SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(),HMAC_ALGORITHM);
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(keySpec);
byte[] rawHmac = mac.doFinal(data.getBytes());
System.out.println(Hex.encodeHexString(rawHmac));
The code produces the following string:
c2812f39f84c32c2edaded339a1388abc9829babf351b684ab797f04cd94d4c7
Through some random search on Shopify developer forum I found the link to a question.
The last message from #Shayne suggests that we have to make changes in data variable by adding protocol field.
But it didn't work out :(
Can anyone tell me what should be done?Do I have to make modifications in my code or the process in the documentation have changed.
Please help.
Here's the java code you need to verify Shopify HMAC. The protocol parameter isn't required unless it was in the result from shopify, which it wasn't from me.
#Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String HMAC_ALGORITHM = "HmacSHA256";
resp.setContentType("text/html;charset=UTF-8");
Map<String,String[]> parameters = req.getParameterMap();
String data = null;
SortedSet<String> keys = new TreeSet<String>(parameters.keySet());
for (String key : keys) {
if (!key.equals("hmac")&&!key.equals("signature")){
if (data == null){
data = key + "=" +req.getParameter(key);
}
else {
data = data + "&" + key + "=" + req.getParameter(key);
}
}
}
SecretKeySpec keySpec = new SecretKeySpec(SHARED_KEY.getBytes(),HMAC_ALGORITHM);
Mac mac = null;
try {
mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(keySpec);
byte[] rawHmac = mac.doFinal(data.getBytes());
if (Hex.encodeHexString(rawHmac).equals(req.getParameter("hmac"))){
//THE HMAC IS VERIFIED
} else {
//THE HMAC IS NOT VERIFIED
}
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
e.printStackTrace();
}
}
Interestingly, the timestamp parameter in data turns into
×tamp=1459537704
instead of
×tamp=1459537704
The example is wrong apparently. Your hash code is OK. You'll need to make sure you include all parameters from the Shopify response e.g. the input for verification of a response would look like:
code={code}&protocol=https://&store={store}×tamp={timestamp}
See: https://ecommerce.shopify.com/c/shopify-apis-and-technology/t/you-broke-my-build-hmac-verification-broken-282951
here is my prod code:
public class HMACValidator {
public static String sha256HMAC(String key, String data) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException, DecoderException {
Mac hmac = Mac.getInstance("HmacSHA256");
System.out.println("data "+data);
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
hmac.init(secret_key);
return Hex.encodeHexString(hmac.doFinal(data.getBytes("UTF-8")));
}
public static boolean validateShopifyAskForPermission(String key, String hmac, String shop, String timestamp) throws Exception {
return (sha256HMAC(key, "shop="+shop+"×tamp="+timestamp).compareTo(hmac) == 0);
}
}
Related
I'm able to successfully get a HMAC SHA256 using the following code:
public static String getHac(String dataUno, String keyUno) throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
SecretKey secretKey = null;
Mac mac = Mac.getInstance("HMACSHA256");
byte[] keyBytes = keyUno.getBytes("UTF-8");
secretKey = new SecretKeySpec(keyBytes,mac.getAlgorithm());
mac.init(secretKey);
byte[] text = dataUno.getBytes("UTF-8");
System.out.println("Hex encode: " + Hex.encode(keyUno.getBytes()));
byte[] encodedText = mac.doFinal(text);
return new String(Base64.encode(encodedText)).trim();
}
which yields:
HMAC: 9rH0svSCPHdbc6qUhco+nlkt2O7HE0rThV4M9Hbv5aY=
However, i would like getting this:
HMAC:eVXBY4RZmFQcOHHZ5FMRjDLOJ8vCuVGTjy7cHN7pqfo=
I tried an online tool and it appears that the difference between my code and online tool is that I am working with a text in the key type.
Test values:
String data = "5515071604000fAIkwJtkeiA:APA91bH_Pb5xB2lrmKWUst5xRuJ3joVE-sb9KoT0zXZuupIEfdHjii-cODj-JMnjyy7hFJUbIRAre9o2yaCU43KaFDmxKlhJhE36Dw0bZ2VntDUn_Zd1EJBuSyCYiUtmmkHfRvRy3hIb";
String key = "fc67bb2ee0648a72317dcc42f232fc24f3964a9ebac0dfab6cf47521e121dc6e";
getHac("5515071604000fAIkwJtkeiA:APA91bH_Pb5xB2lrmKWUst5xRuJ3joVE-sb9KoT0zXZuupIEfdHjii-cODj-JMnjyy7hFJUbIRAre9o2yaCU43KaFDmxKlhJhE36Dw0bZ2VntDUn_Zd1EJBuSyCYiUtmmkHfRvRy3hIb", "fc67bb2ee0648a72317dcc42f232fc24f3964a9ebac0dfab6cf47521e121dc6e"));
the execution of my method return
9rH0svSCPHdbc6qUhco+nlkt2O7HE0rThV4M9Hbv5aY=
(the online returns the same value with key type text selected)
and i expected
eVXBY4RZmFQcOHHZ5FMRjDLOJ8vCuVGTjy7cHN7pqfo=
(the online returns the same value with key type hex selected)
Assuming that you are using Apache Commons Codec 1.11, use the following:
byte[] keyBytes = Hex.decodeHex(keyUno);
getHac Method
You code just slightly modified looks like this then:
public static String getHac(String dataUno, String keyUno)
throws InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException, DecoderException {
SecretKey secretKey;
Mac mac = Mac.getInstance("HMACSHA256");
byte[] keyBytes = Hex.decodeHex(keyUno);
secretKey = new SecretKeySpec(keyBytes, mac.getAlgorithm());
mac.init(secretKey);
byte[] text = dataUno.getBytes("UTF-8");
byte[] encodedText = mac.doFinal(text);
return new String(Base64.encodeBase64(encodedText)).trim();
}
Test
This Java method gives then expected result:
eVXBY4RZmFQcOHHZ5FMRjDLOJ8vCuVGTjy7cHN7pqfo=
I am trying to send a POST request to the coinbase sandbox endpoint. When signing the request I always get an "Invalid Signature" response. It seems that coinbase requires the JSON message be base 64 encoded and sent as a part of the signature. I am fairly new to POST requests and have never signed a message before. Can someone please let me know what I am doing wrong. I have been stuck on this issue for a week so any input is much appreciated.
The relevant part of my code is below
public void postOrder() throws InvalidKeyException, NoSuchAlgorithmException, CloneNotSupportedException, ClientProtocolException, IOException {
String message = "{ \n"+
" \"size\":\"1.00\", \n"+
" \"price\":\"0.80\", \n"+
" \"side\":\"buy\", \n"+
" \"product_id\":\"BTC-USD\" \n"+
"}";
JSONObject json = new JSONObject(message);
message = json.toString();
try
{
String timestamp= Instant.now().getEpochSecond()+"";
String accessSign = getAccess(timestamp,"POST","/orders",message);
String apiKey = properties.getProperty("key");
String passphrase = properties.getProperty("passphrase");
URL url = new URL("https://api-public.sandbox.pro.coinbase.com/orders");
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
connection.setDoOutput(true);
connection.setRequestProperty("accept", "application/json");
connection.setRequestProperty("content-type", "application/json; charset=UTF-8");
connection.setRequestProperty("CB-ACCESS-KEY", apiKey);
connection.setRequestProperty("CB-ACCESS-SIGN", accessSign);
connection.setRequestProperty("CB-ACCESS-TIMESTAMP", timestamp);
connection.setRequestProperty("CB-ACCESS-PASSPHRASE", passphrase);
connection.setRequestProperty("User-Agent", "Java Client");
try {
connection.getOutputStream().write(message.getBytes("UTF-8"));
OutputStream output = connection.getOutputStream();
output.write(param.getBytes("UTF-8"));
} catch (Exception e) {
System.out.println(e.getMessage());
}
String status = connection.getResponseMessage();
System.out.println("STATUS: "+status);
}catch(Exception e) {
System.out.println(e.getMessage());
}
return;
}
private String getAccess(String timestamp, String method, String path, String param) throws NoSuchAlgorithmException, InvalidKeyException, CloneNotSupportedException, IllegalStateException, UnsupportedEncodingException {
String secretKeyString = properties.getProperty("secret");
String prehash = timestamp+method+path+param;
byte[] secretKeyDecoded = Base64.getDecoder().decode(secretKeyString);
SecretKey secretKey = new SecretKeySpec(secretKeyDecoded, "HmacSHA256");
Mac hmacSha256 = Mac.getInstance("HmacSHA256");
hmacSha256.init(secretKey);
return Base64.getEncoder().encodeToString(hmacSha256.doFinal(prehash.getBytes()));
}
I was able to get this working with using javax.crypto.* library functions. The changes I did were
Took the API Key for the sandbox.
To encoded the signature, I explicitly used UTF_8
Below is the code that worked for me with Coinbase sandbox API_KEY -
public String getCoinbaseHeaderSignature(
String timeStamp,
String method,
String requestPath,
String body
) throws NoSuchAlgorithmException, InvalidKeyException {
String data = timeStamp + method.toUpperCase() + requestPath + body;
byte[] key = Base64.getDecoder().decode(API_KEY);
SecretKey keySpec = new SecretKeySpec(key, "HmacSHA256");
// Get HmacSHA256 instance
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(keySpec);
return Base64.getEncoder().encodeToString(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}
For more details, refer to documentation https://docs.cloud.coinbase.com/sign-in-with-coinbase/docs/api-key-authentication
In my case, I utilized gdax-java example. I solved this issue by removing decimal values from timestamp that means I just used integer part of the timestamp value.
I am having problems generating a valid signature key for doing HTTP Post in browser- I keep getting errors like
The request signature we calculated does not match the signature you provided. Check your key and signing method
and since that doesn't tell me anything much about where the problem lies, i decided to debug if my signature calculation method is correct.
So I decided to replicate the example in the documentation using the given String to sign and the Secret Access Key. My generated signature doesn't match the signature given on the documentation page.
What am I doing wrong here ? The below is my existing code (I am using AWS Signature V4)
private void debugSignatureGeneration() throws Exception {
byte[] testSigningKey = getSigningKey("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "20151229", "us-east-1",
"s3");
String testStringToSign = "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJzaWd2NGV4YW1wbGVidWNrZXQifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwNCiAgICB7ImFjbCI6ICJwdWJsaWMtcmVhZCJ9LA0KICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL3NpZ3Y0ZXhhbXBsZWJ1Y2tldC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiaW1hZ2UvIl0sDQogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwNCiAgICB7IngtYW16LXNlcnZlci1zaWRlLWVuY3J5cHRpb24iOiAiQUVTMjU2In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIkeC1hbXotbWV0YS10YWciLCAiIl0sDQoNCiAgICB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LA0KICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwNCiAgICB7IngtYW16LWRhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfQ0KICBdDQp9";
String testSignature = getSignatureV4(testStringToSign, testSigningKey);
System.out.println("test signature " + testSignature);
}
static String getSignatureV4(String stringToSign, byte[] signingKey) throws Exception {
byte[] signature = HmacSHA256(stringToSign, signingKey);
return Hex.encodeHexString(signature);
}
static byte[] HmacSHA256(String policy, byte[] key) throws Exception {
String algorithm = "HmacSHA256";
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(policy.getBytes("UTF8"));
}
static byte[] getSigningKey(String key, String dateStamp, String regionName, String serviceName) throws Exception {
byte[] kSecret = ("AWS4" + key).getBytes("UTF8");
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
If it helps, the signature that is given in the documentation is
46503978d3596de22955b4b18d6dfb1d54e8c5958727d5bdcd02cc1119c60fc9
while the signature I am able to get is
8afdbf4008c03f22c2cd3cdb72e4afbb1f6a588f3255ac628749a66d7f09699e
I'm having trouble fetching mutual friends for two users of my app who are not friends.
As per all_mutual_friends permission, I need to make the request along with the appsecret_proof parameter.
I generated the app_access_token using this GET call:
GET /oauth/access_token
?client_id={app-id}
&client_secret={app-secret}
&grant_type=client_credentials
I've triple checked the app_id and app_secret, they are correct. I generated the appsecret_proof by SHA256 hashing the app_access_token with app_secret in Java.
Now when I request the mutual friends (sending the appsecret_proof as query parameter), it responds saying
"Invalid appsecret_proof provided in the API argument"
with a GraphMethodException. The original request (without appsecret_proof) is working fine for users who are friends. Any pointers here?
Here is the java code I'm using to generate appsecret_proof:
public static String hashMac(String text, String secretKey)
throws SignatureException {
try {
Key sk = new SecretKeySpec(secretKey.getBytes(), HASH_ALGORITHM);
Mac mac = Mac.getInstance(sk.getAlgorithm());
mac.init(sk);
final byte[] hmac = mac.doFinal(text.getBytes());
return toHexString(hmac);
} catch (NoSuchAlgorithmException e1) {// throw an exception or pick a different encryption method
throw new SignatureException(
"error building signature, no such algorithm in device "
+ HASH_ALGORITHM);
} catch (InvalidKeyException e) {
throw new SignatureException(
"error building signature, invalid key " + HASH_ALGORITHM);
}
}
private static final String HASH_ALGORITHM = "HmacSHA256";
public static String toHexString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
Formatter formatter = new Formatter(sb);
for (byte b : bytes) {
formatter.format("%02x", b);
}
return sb.toString();
}
My server is python based.
I was able to fetch the mutual friends. I was using app_access_token to generate the appsecret_proof but the access_token of sessioned user needs to be used to generate the appsecret_proof. Apparently, this was not documented by Facebook.
I am trying to consume a web service that uses Password Digest mode, and I have these functions in my Java application to generate a random nonce, creation date and password digest. I can't get past the Authentication Failed error, and the documentation isn't overly clear on whether they want SHA-1 or MD5, as it mentions both in passing. I've tried MD5 instead of SHA-1 and I am getting the same result. I managed to get the requests to work via a test on SoapUI, but I have no idea how that application is generating the digest / nonce. Any help is appreciated.
Here's the code I am using to generate the nonce and the password digest:
private static SOAPMessage createSOAPRequest() throws Exception
{
String password = "FakePassword";
String nonce = generateNonce();
System.out.println("Nonce = " + nonce);
DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
Date today = Calendar.getInstance().getTime();
String created = dateFormatter.format(today);
System.out.println("Created = " + created);
String passwordDigest = buildPasswordDigest(nonce, created, password);
System.out.println("Password Digest = " + passwordDigest);
}
private static String buildPasswordDigest(String nonce, String created, String password) throws NoSuchAlgorithmException, UnsupportedEncodingException
{
MessageDigest sha1;
String passwordDigest = null;
try
{
sha1 = MessageDigest.getInstance("SHA-1");
sha1.update(Base64.decodeBase64(nonce));
sha1.update(created.getBytes("UTF-8"));
passwordDigest = new String(Base64.encodeBase64(sha1.digest(password.getBytes("UTF-8"))));
sha1.reset();
}
catch (NoSuchAlgorithmException e)
{
e.printStackTrace();
}
return passwordDigest;
}
private static String generateNonce() throws NoSuchAlgorithmException, NoSuchProviderException, UnsupportedEncodingException
{
String dateTimeString = Long.toString(new Date().getTime());
byte[] nonceByte = dateTimeString.getBytes();
return Base64.encodeBase64String(nonceByte);
}
The solution was to replace the line sha1.update(nonce.getBytes("UTF-8")); with sha1.update(Base64.decodeBase64(nonce));