I'm implementing interactive messages on Slack, which contains some action buttons. Using Slack App I'm able to handle Slack users clicking the buttons on my Java Springboot API.
To this moment, everything is fine. However, I struggle to compute matching request signature (digest) to verify, that it actually comes from Slack. I read all the documentation for that on Slack verification documentation page.
The page decribes, that the signature has to be computed as a HMAC SHA256 hash, using Signing Secret as a key and content as concatenation of slack version, timestamp and request body, for example:
v0:123456789:command=/weather&text=94070
On the page is stated:
...Evaluate only the raw HTTP request body when computing signatures.
... so I'm not encoding/deserializing the request before hash computing (I've attached my received request from Slack below)
To compute the hash I use the code found on StackOverflow:
private String computeMessageDigest(String content) {
final String ALGORITHM = "HmacSHA256";
final String UTF_8 = "UTF-8";
try {
Key signingKey = new SecretKeySpec(signingSecret.getBytes(UTF_8), ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
return Hex.encodeHexString(mac.doFinal(content.getBytes(UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
I tried also this online hash generator to compare the results, and they were the same.
The request received from Slack looks like this:
{
"headers": {
"x-forwarded-for": ["::ffff:52.72.111.29"],
"x-forwarded-proto": ["https"],
"x-pagekite-port": ["443"],
"host": ["inqool.pagekite.me"],
"user-agent": ["Slackbot 1.0 (+https://api.slack.com/robots)"],
"accept-encoding": ["gzip,deflate"],
"accept": ["application/json,*/*"],
"x-slack-signature": ["v0=87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8"],
"x-slack-request-timestamp": ["1531221943"],
"content-length": ["2731"],
"Content-Type": ["application/x-www-form-urlencoded;charset=UTF-8"]
},
"body": "payload=%7B%22type%22%3A%22interactive_message%22%2C%22actions%22%3A%5B%7B%22name%22%3A%22reject_btn%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22false%22%7D%5D%2C%22callback_id%22%3A%22artwork%3D40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22team%22%3A%7B%22id%22%3A%22T03NP6SA7%22%2C%22domain%22%3A%22artstaq%22%7D%2C%22channel%22%3A%7B%22id%22%3A%22G8F2WR4FJ%22%2C%22name%22%3A%22privategroup%22%7D%2C%22user%22%3A%7B%22id%22%3A%22U66T9QX60%22%2C%22name%22%3A%22majo%22%7D%2C%22action_ts%22%3A%221531221943.512498%22%2C%22message_ts%22%3A%221531221198.000225%22%2C%22attachment_id%22%3A%221%22%2C%22token%22%3A%22ZABrZDXgJCOOLNau5mXnfNQR%22%2C%22is_app_unfurl%22%3Afalse%2C%22original_message%22%3A%7B%22text%22%3A%22User+just+put+item+on+*EXCHANGE*.%22%2C%22bot_id%22%3A%22BBM1W4QEL%22%2C%22attachments%22%3A%5B%7B%22author_name%22%3A%22Slack+Test%3B+slack%40test.com%22%2C%22callback_id%22%3A%22artwork%3D40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22fallback%22%3A%22Slack+Test%3B+%3Cmailto%3Aslack%40test.com%7Cslack%40test.com%3E+just+put+item+Panenka+%5C%2F+Doll+by+artist+Jaroslav+Vale%5Cu010dka+into+ON+REQUEST+mode%22%2C%22text%22%3A%22%3Chttp%3A%5C%2F%5C%2Flocalhost%3A8080%5C%2Fartist%5C%2F609cd328-d533-4ab0-b982-ec2f104476f2%7CJaroslav+Vale%5Cu010dka%3E%22%2C%22title%22%3A%22Panenka+%5C%2F+Doll%22%2C%22footer%22%3A%22ARTSTAQ+Slack+Reporter%22%2C%22id%22%3A1%2C%22title_link%22%3A%22http%3A%5C%2F%5C%2Flocalhost%3A8080%5C%2Fartwork%5C%2F40d7a87f-466c-4fc9-b454-09ce020d4465%22%2C%22color%22%3A%22f0d0ad%22%2C%22fields%22%3A%5B%7B%22title%22%3A%22Trading+type%22%2C%22value%22%3A%22ON+REQUEST%22%2C%22short%22%3Atrue%7D%5D%2C%22actions%22%3A%5B%7B%22id%22%3A%221%22%2C%22name%22%3A%22approve_btn%22%2C%22text%22%3A%22APPROVE%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22true%22%2C%22style%22%3A%22primary%22%2C%22confirm%22%3A%7B%22text%22%3A%22Do+you+really+want+to+approve+this+artwork%3F%22%2C%22title%22%3A%22Approve+artwork%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22Cancel%22%7D%7D%2C%7B%22id%22%3A%222%22%2C%22name%22%3A%22reject_btn%22%2C%22text%22%3A%22REJECT%22%2C%22type%22%3A%22button%22%2C%22value%22%3A%22false%22%2C%22style%22%3A%22danger%22%2C%22confirm%22%3A%7B%22text%22%3A%22Do+you+really+want+to+reject+this+artwork%3F%22%2C%22title%22%3A%22Reject+artwork%22%2C%22ok_text%22%3A%22Yes%22%2C%22dismiss_text%22%3A%22Cancel%22%7D%7D%5D%7D%5D%2C%22type%22%3A%22message%22%2C%22subtype%22%3A%22bot_message%22%2C%22ts%22%3A%221531221198.000225%22%7D%2C%22response_url%22%3A%22https%3A%5C%2F%5C%2Fhooks.slack.com%5C%2Factions%5C%2FT03NP6SA7%5C%2F395760858899%5C%2FGlP9jsNQak7FqEciEHhscx4L%22%2C%22trigger_id%22%3A%22395632563524.3771230347.851ab60578de033398338a9faeb41a15%22%7D"
}
When I computed the HMAC SHA256 hash, I got 561034bb6860c07a6b4eaf245b6da3ea869c7806c7f7be20b1a830b6d25c54c8
but I should get 87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8
, as in the request header.
I also tried to compute the hash from the URL decoded body, but still not be able to get the matching signature.
Am I doing something wrong? Thanks for the answers/hints.
EDIT: here's the whole source code of my REST controller and request verifier:
package com.artstaq.resource;
import com.artstaq.integration.slack.SlackRequestVerifier;
import org.springframework.http.HttpEntity;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.inject.Inject;
@RestController
@RequestMapping("/content_admin")
public class ContentAdminResource {
private SlackRequestVerifier slackVerifier;
@RequestMapping(value = "/slack/artwork/resolve", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void resolve(HttpEntity<String> request) {
slackVerifier.verifySlackRequest(request);
}
@Inject
public void setSlackVerifier(SlackRequestVerifier slackVerifier) {
this.slackVerifier = slackVerifier;
}
}
package com.artstaq.integration.slack;
import com.artstaq.exception.SignatureVerificationException;
import com.artstaq.exception.TimestampTooOldException;
import org.apache.commons.codec.binary.Hex;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.stereotype.Component;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
/**
* Class providing request verification received from Slack
*/
@Component
public class SlackRequestVerifier {
@Value("${integration.slack.version:v0}")
private String version;
@Value("${integration.slack.signingSecret}")
private String signingSecret;
/**
* Verifies the integrity of received Slack request.
*/
public void verifySlackRequest(HttpEntity<String> request) {
String timestamp = request.getHeaders().getFirst(SlackHeaders.TIMESTAMP);
Instant timeInstant = Instant.ofEpochSecond(Long.valueOf(timestamp));
if (timeInstant.plus(5, ChronoUnit.MINUTES).compareTo(Instant.now()) < 0) {
throw new TimestampTooOldException(timeInstant);
}
String expectedDigest = request.getHeaders().getFirst(SlackHeaders.SIGNATURE);
String basestring = String.join(":", version, timestamp, request.getBody());
String computedDigest = version + "=" + computeMessageDigest(basestring);
if (!computedDigest.equals(expectedDigest)) {
throw new SignatureVerificationException(expectedDigest, computedDigest);
}
}
/**
* Compute HMAC SHA256 digest for given content using defined slack signing secret
*/
private String computeMessageDigest(String content) {
final String ALGORITHM = "HmacSHA256";
final String UTF_8 = "UTF-8";
try {
Key signingKey = new SecretKeySpec(signingSecret.getBytes(UTF_8), ALGORITHM);
Mac mac = Mac.getInstance(ALGORITHM);
mac.init(signingKey);
return Hex.encodeHexString(mac.doFinal(content.getBytes(UTF_8)));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static class SlackHeaders {
private static final String TIMESTAMP = "X-Slack-Request-Timestamp";
private static final String SIGNATURE = "X-Slack-Signature";
}
}