Slack Commands & AWS Serverless

Krishnan Viswanath
Mar 22 · 4 min read

The idea of this simple utility is ties a Slack commnad to AWS Serverless. This narrow scoped example integrates Slack and AWS Serverless. This touches on key areas and hopefully provides insights on how these technologies could be combined to add meaningful value to the team (and business)

High Level Architecture

Note this is Java centric

Setting up a Spring Boot based AWS Serverless

With Maven is available execute the following command

mvn archetype:generate -DgroupId=com.awsserverless -DartifactId=slack -Dversion=1.0-SNAPSHOT \
-DarchetypeGroupId=com.amazonaws.serverless.archetypes \
-DarchetypeArtifactId=aws-serverless-springboot2-archetype \
-DarchetypeVersion=1.3.2

The above archetype sets up a new aws serverless maven project. Apom.xml is generated that includes the dependencies needed to build a basic Spring Boot API. The application is based on the spring-boot-starter-web. The generated code includes a StreamLambdaHandler class, the main entry point for AWS Lambda; an Application class that defines a basic Spring Boot application; Most importantly, the project also includes sam.yam. This is a SAM template that you can be used to test application in local or deploy it to AWS.

Writing Endpoint to manage request from Slack

Create an endpoint. Few things of interest. X-Slack-Request-Timestamp, X-Slack-Signature and requestBody. These are sent from Slack; otherwise everything is typical

@RestController
public class SlackController {
private static final Logger l =
LoggerFactory.getLogger(SlackController.class);
@RequestMapping(path = "/ping", method = RequestMethod.POST)
public ResponseEntity<?> ping(
@RequestHeader(value = "X-Slack-Request-Timestamp") String timeStamp, @RequestHeader(value = "X-Slack-Signature") String signature, @RequestBody String requestBody ) {
// Verfiy the request; make sure it is legit
// and repond back
// This util simply goes through the requestBody and prepare
// POJO with the data
SlackData slackData =
SlackUtil.parseRequestForSlackData(requestBody);

SlackVerified slackVerified =
slackRequestVerifier.verify(config.getSlackSigningSecret(),
timeStamp, signature, slackData );

return new ResponseEntity<>("Hello Worldr",
slackVerified.getSigningVerification() ==
SlackVerified.SigningVerification.VERIFIED ? HttpStatus.OK
: HttpStatus.I_AM_A_TEAPOT);
}

Validate the Slack Request

The following piece of code verifies that the Slack request is infact legitimate; note in case of AWS Gateway (serverless), the request body ordering change is changed; as a result the verification will fail; so this needs to be reassembled into right order to get it properly verified

import io.vavr.collection.List;
import io.vavr.control.Try;
....public SlackVerified verify(String signingSecret, String timeStamp,
String signature, SlackData slackData ){
SlackVerified slackVerified = new SlackVerified();
try {
// Replay check;
Instant timeInstant =
Instant.ofEpochSecond(Long.valueOf(timeStamp));
if (timeInstant.plus(5,
ChronoUnit.MINUTES).compareTo(Instant.now()) < 0) {

slackVerified.setSigningVerification(SigningVerification.DENIED); slackVerified.setParameterVerification(ParameterVerification.NOT_OK);
return slackVerified;
}

// AWS Gateway reorders the body; so have to get that fixed
String slackExpectedRequestBody = fixRequestBody(slackData);

String basestring = String.join(":", "v0", timeStamp,
slackExpectedRequestBody);
SecretKeySpec secretKey = Try.of(() -> new
SecretKeySpec(signingSecret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256")).get();
Mac sha256HMAC = Try.of(() ->
Mac.getInstance("HmacSHA256")).get();
Try.run(() -> sha256HMAC.init(secretKey));
String hash = Try.of(() ->"v0=" +
Hex.encodeHexString(sha256HMAC.doFinal(basestring.trim()
.getBytes(StandardCharsets.UTF_8)))).get();

slackVerified.setSigningVerification(hash.equals(signature) ?
SigningVerification.VERIFIED : SigningVerification.DENIED);
slackVerified.setParameterVerification(Util.validate(slackData.
getText()) ? ParameterVerification.OK :
ParameterVerification.NOT_OK);

} catch (Exception e) {
slackVerified.setSigningVerification(
SigningVerification.DENIED);
slackVerified.setParameterVerification(ParameterVerification.
NOT_OK);
}
return slackVerified;
}
private String fixRequestBody(SlackData slackData) throws
IllegalArgumentException, IllegalAccessException {
Class<?> clazz = slackData.getClass();
Map<String, String> slackElementsMap = new HashMap<>();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(SlackElement.class)) {
slackElementsMap.put(getKey(field),
(String) field.get(slackData));
}
}

List<String> names = List.of("token", "team_id", "team_domain",
"channel_id", "channel_name" , "user_id", "user_name" ,
"command", "text", "response_url" , "trigger_id" );

// Need to look like this
token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J
&team_domain=testteamnow
&channel_id=G8PSS9T3V&channel_name=foobar
&user_id=U2CERLKJA&user_name=roadrunner
&command=%2Fwebhook-collect&text=
&response_url=https%3A%2F%2Fhooks.slack.com%2F
commands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRs
kXaIFfN&trigger_id=398738663015.47445629121.803a0bc887
a14d10d2c447fce8b6703c

return names.map(name -> name + "=" +
UriUtils.encode(slackElementsMap.
get(name), "UTF-8")).mkString("&");
}

Create SAM

With code written up, next step is to create SAM (Serverless Application Model); leaving out param and output

Resources:
CpDeployFunction:
Type: AWS::Serverless::Function
Properties:
Handler: com.hyla.deploy.StreamLambdaHandler::handleRequest
Runtime: java8
CodeUri: target/util-1.0-SNAPSHOT-lambda-package.zip
MemorySize: 512
Policies:
- AWSLambdaBasicExecutionRole
Timeout: 30
Events:
GetResource:
Type: Api
Properties:
Path: /{proxy+}
Method: post

Outputs:
CpDeployApi:
Description: URL for application
Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/ping'
Export:
Name: CpDeployApi

Deploy

mvn clean package
aws cloudformation package --template-file sam.yaml --output-template-file output-sam.yaml --s3-bucket [[bucket]]
aws cloudformation deploy --template-file output-sam.yaml --stack-name [[Stack Name]]

Setting up Slack

From https://api.slack.com/ log into your apps and set up slash command. The request URL is the API Gateway output

set up a slash command

Testing it from Slack

From a slack channel, execute the command to get the output

output from serverless lambda

Closing

Hope this high level flyby of setting up slack and aws serverless was useful. If there are any specific questions on this topic feel free send me an email

Thank you reading.

More From Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade