Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #192 ExpressReceiver support for rawBody for signature verification #197

Merged
merged 2 commits into from
May 24, 2019

Conversation

seratch
Copy link
Member

@seratch seratch commented May 19, 2019

Summary

This pull request fixes issue #192 which prevents Bolt running on Google Cloud Platform.

I've extracted verifySlackRequest method from ExpressReceiver as ExpressSignatureVerifier and modified it a litle. Also, I changed the body parser parseBody in ExpressReceiver to handle rawBody. I'll add line comments to explain the intention further.

I haven't added unit tests for this change yet but I made sure the change works on real GCP environment. My example app on GCP managed to respond to url_verification requests.
UPDATE: My example app on GCP responded to url_verification requests and handled events as I expect.

Separately from that, it seems that we still have another issue after the body parsing step. My app on GCP didn't work for event subscription (probably other requests, too) due to ack timeout errors. I've not figured out the root cause yet, but it seems to be hanging here.
^ UPDATE: Never mind. It was just my personal projects' quota issue...

Requirements (place an x in each [ ])

@seratch seratch requested a review from aoberoi May 19, 2019 19:10
@codecov
Copy link

codecov bot commented May 19, 2019

Codecov Report

Merging #197 into master will increase coverage by 4.06%.
The diff coverage is 80.48%.

Impacted file tree graph

@@            Coverage Diff             @@
##           master     #197      +/-   ##
==========================================
+ Coverage   63.82%   67.88%   +4.06%     
==========================================
  Files           7        7              
  Lines         445      464      +19     
  Branches      122      126       +4     
==========================================
+ Hits          284      315      +31     
+ Misses        153      139      -14     
- Partials        8       10       +2
Impacted Files Coverage Δ
src/App.ts 83.66% <100%> (ø) ⬆️
src/ExpressReceiver.ts 55.23% <80%> (+23.84%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 44ca9d5...7799510. Read the comment docs.

@@ -40,7 +39,7 @@ export default class ExpressReceiver extends EventEmitter implements Receiver {
this.server = createServer(this.app);

const expressMiddleware: RequestHandler[] = [
verifySlackRequest(signingSecret),
ExpressSignatureVerifier.create(signingSecret),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you prefer other names, please let me know. I don't have any strong opinions on this.

} else {
// TODO: should we check the content type header to make sure its JSON here?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worked on this TODO but I'm willing to revert/modify this.

'Slack request signing verification failed. Timestamp is too old.',
ErrorCode.ExpressReceiverAuthenticityError,
);
return next(error);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realized that, even in this case, verifySlackRequest showed the following other errors. So, I added return here. If having return here has been intentionally avoided, let me know.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! This must have been a bug.


const signature = req.headers['x-slack-signature'] as string;
const ts = Number(req.headers['x-slack-request-timestamp']);
if (!signature && !ts) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a newly added validation.

'Slack request signing verification failed. Signature mismatch.',
ErrorCode.ExpressReceiverAuthenticityError,
);
return next(error);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, a good catch! In the past, TypeScript has complained when I return the value returned by next(), and my solution was to just return; on its own on the next line. I'm surprised this is passing the type checker, but great!

if (preparsedRawBody) {
stringBody = preparsedRawBody.toString();
} else {
stringBody = rawBody(req).toString();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bug here. I'm writing tests and will fix this code, too.

@seratch seratch changed the title Fix #192 ExpressReceiver support for rawBody for signature verification WIP: Fix #192 ExpressReceiver support for rawBody for signature verification May 20, 2019
@seratch seratch changed the title WIP: Fix #192 ExpressReceiver support for rawBody for signature verification Fix #192 ExpressReceiver support for rawBody for signature verification May 20, 2019
@seratch
Copy link
Member Author

seratch commented May 20, 2019

This pull request is now ready to be reviewed. I avoided doing force-push and pushed several commits. Please squash them into one when merging it.


export default class ExpressSignatureVerifier {

public static create(signingSecret: string): RequestHandler {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand the benefit of using a class with a static method for this functionality? I totally see the benefit of encapsulating this into a testable unit of code.

In my experience with the JavaScript community, using a class in this case is not idiomatic. There's no instance with state, so this can be effectively represented with just a function, right? It seems like signingSecret is state of an "instance" of the returned function, so if we wanted to use OOP design you should initialize an instance with a signingSecret and then use a method as the interface with the Express router. But I wouldn't recommend an OOP design here. It seems like a higher order function (as create() already is) is totally fine without embedding in a class.

At that point I don't see any benefit of moving this code out of ExpressReceiver.ts, since it is tightly coupled to Express.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the comment. Yes, I have to admit this seems to be in a very other-lang style. 😅

Anyway, as we discussed here, I'll merge this functionality into a single RequestHandler (which means merging this verification and parsing body process into one).

@@ -151,58 +150,52 @@ const respondToUrlVerification: RequestHandler = (req, res, next) => {
next();
};

// TODO: this should be imported from another package
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this TODO comment could have been confusing, and might have led to the choice of creating an ExpressSignatureVerifier class. I'd like to clarify.

I meant that we should produce another package, maybe inside the Node Slack SDK repo, which could export a function like verifyRequestSignature(), as seen in the @slack/events-api package: https://github.com/slackapi/node-slack-sdk/blob/8d3b2fdef099306732b344c7e030dd09c82dd91b/packages/events-api/src/http-handler.js#L22-L58

It might also export other useful utilities or functions. One idea would be a to expose an express middleware like the function below this line, which uses the verifyRequestSignature() described above. If this package was distributed separately, (maybe @slack/verify-request), then people who don't use Bolt, or Express, or any other tool we provide could still use our request verification logic, and we would only have to maintain it once (instead of several places like it is today).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this package was distributed separately, (maybe @slack/verify-request), then people who don't use Bolt, or Express, or any other tool we provide could still use our request verification logic, and we would only have to maintain it once (instead of several places like it is today).

Sounds great. In this pull request, let me go with a necessary and sufficient patch for the issue happening on GCP. Afterwards, I'd love to be involved in that improvement, too.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I agree that we can come back on a separate pass to complete the extraction into a new package.

* This handler parses `req.body` or `req.rawBody`(on Google Could Platform)
* and overwrites `req.body` with the parsed JS object.
* Following middlewares can expect `req.body` is no long a string value.
*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments are excellent!

// On some environments like GCP (Google Cloud Platform),
// req.body can be pre-parsed and be passed as req.rawBody here
const preparsedRawBody: any = (req as any).rawBody;
if (preparsedRawBody) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try not to use truthiness or falsiness in conditions like this. Instead, we can use preparsedRawBody !== undefined.

* Following middlewares can expect `req.body` is no long a string value.
*/
const parseBody: RequestHandler = async (req, _res, next) => {
const stringBody: string = await extractRequestBodyAsString(req);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this being called here instead of just accessing req.body? it seems that the ExpressSignatureVerifier will call this same function first, and assign a value back to req.body.

In fact, I think there are two concepts that we're trying to separate, but they are very coupled: request verification, and body parsing. Since they are so coupled, would it make sense to implement them in one RequestHandler? The one handler could call a helper function that knows how to verify the signature given a body Buffer, and a signing secret, a current time, and the values from the request header (signature and timestamp). Once the signature is verified, the Buffer contents can be trusted, so it's safe to parse the Buffer into an object and assign the result to req.body. I feel like the intermediate string representation isn't useful, and can be eliminated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the clear explanation. Now I understand the point, and totally agree with the plan described here. I was also thinking that having temporary string representation is a bit confusing and actually has no benefit. I'm going to merge the two into one RequestHandler named verifySignatureAndParseBody (let me know if you have a better name for it).

} else {
// TODO: should we check the content type header to make sure its JSON here?
req.body = JSON.parse(req.body);
console.warn(`Unexpected content-type detected: ${contentType}`);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a great idea! Let's try not to use the console global from this file, and instead use a Logger instance. Since there isn't one available directly here, maybe this sort of concern should be the responsibility of the ExpressReceiver class (we can add a logger option to the constructor).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion. Will do.

// Parse this body anyway
req.body = JSON.parse(stringBody);
} catch (e) {
console.error(`Failed to parse body as JSON data for content-type: ${contentType}`)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

'Slack request signing verification failed. Timestamp is too old.',
ErrorCode.ExpressReceiverAuthenticityError,
);
return next(error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! This must have been a bug.

'Slack request signing verification failed. Signature mismatch.',
ErrorCode.ExpressReceiverAuthenticityError,
);
return next(error);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, a good catch! In the past, TypeScript has complained when I return the value returned by next(), and my solution was to just return; on its own on the next line. I'm surprised this is passing the type checker, but great!

@aoberoi
Copy link
Contributor

aoberoi commented May 20, 2019

@seratch This is really thorough and impressive. I have some comments and questions regarding the design, so I left them in this review. I did not look too closely at the tests because I think you might agree with some of the feedback, and those tests might change along with the design. Happy to hear your thoughts.

@seratch
Copy link
Member Author

seratch commented May 21, 2019

@aoberoi Thank you very much for your generous review. I've updated the PR again.

Update summary:

  • I merged verifySlackRequest and parseBody into unified RequestHandler named verifySignatureAndParseBody. One thing I should notice you is that I exported the function because I wanted to have tests for it. If you have suggestions or alternatives, please let me know.
  • I added logger as an optional arg to ExpressReceiverOptions and ExpressReceiver's constructor args + use it to output warnings/errors
  • The function verifyRequestSignature is not ready to be extracted as a separate module but it's not exposed to external. Therefore, we can freely change the interface when extracting it as a reusable module.

signingSecret = '',
endpoints = { events: '/slack/events' },
logger = new ConsoleLogger(),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can App pass in a logger when initializing this object? That way the logLevel is respected all the way down.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing out this. I should have been more careful about it... Anyway, I've updated the branch to fix this.

@aoberoi
Copy link
Contributor

aoberoi commented May 24, 2019

This looks so good! Thanks especially for designing a testing solution for Express-related concerns, that looked tricky.

@aoberoi aoberoi merged commit 77431a7 into slackapi:master May 24, 2019
@seratch seratch deleted the issue-192-raw-body branch June 20, 2020 00:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants