-
Notifications
You must be signed in to change notification settings - Fork 399
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
Conversation
Codecov Report
@@ 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
Continue to review full report at Codecov.
|
src/ExpressReceiver.ts
Outdated
@@ -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), |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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.
src/ExpressSignatureVerifier.ts
Outdated
'Slack request signing verification failed. Timestamp is too old.', | ||
ErrorCode.ExpressReceiverAuthenticityError, | ||
); | ||
return next(error); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
src/ExpressSignatureVerifier.ts
Outdated
|
||
const signature = req.headers['x-slack-signature'] as string; | ||
const ts = Number(req.headers['x-slack-request-timestamp']); | ||
if (!signature && !ts) { |
There was a problem hiding this comment.
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.
src/ExpressSignatureVerifier.ts
Outdated
'Slack request signing verification failed. Signature mismatch.', | ||
ErrorCode.ExpressReceiverAuthenticityError, | ||
); | ||
return next(error); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same as above
There was a problem hiding this comment.
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!
src/ExpressReceiver.ts
Outdated
if (preparsedRawBody) { | ||
stringBody = preparsedRawBody.toString(); | ||
} else { | ||
stringBody = rawBody(req).toString(); |
There was a problem hiding this comment.
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.
61b22ec
to
698f283
Compare
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. |
src/ExpressSignatureVerifier.ts
Outdated
|
||
export default class ExpressSignatureVerifier { | ||
|
||
public static create(signingSecret: string): RequestHandler { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These comments are excellent!
src/ExpressReceiver.ts
Outdated
// 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) { |
There was a problem hiding this comment.
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
.
src/ExpressReceiver.ts
Outdated
* 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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).
src/ExpressReceiver.ts
Outdated
} 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}`); |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
src/ExpressReceiver.ts
Outdated
// Parse this body anyway | ||
req.body = JSON.parse(stringBody); | ||
} catch (e) { | ||
console.error(`Failed to parse body as JSON data for content-type: ${contentType}`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above.
src/ExpressSignatureVerifier.ts
Outdated
'Slack request signing verification failed. Timestamp is too old.', | ||
ErrorCode.ExpressReceiverAuthenticityError, | ||
); | ||
return next(error); |
There was a problem hiding this comment.
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.
src/ExpressSignatureVerifier.ts
Outdated
'Slack request signing verification failed. Signature mismatch.', | ||
ErrorCode.ExpressReceiverAuthenticityError, | ||
); | ||
return next(error); |
There was a problem hiding this comment.
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!
@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. |
@aoberoi Thank you very much for your generous review. I've updated the PR again. Update summary:
|
7efc90b
to
6903778
Compare
6903778
to
6cea077
Compare
signingSecret = '', | ||
endpoints = { events: '/slack/events' }, | ||
logger = new ConsoleLogger(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
This looks so good! Thanks especially for designing a testing solution for Express-related concerns, that looked tricky. |
13ca595
to
7799510
Compare
Summary
This pull request fixes issue #192 which prevents Bolt running on Google Cloud Platform.
I've extracted
verifySlackRequest
method fromExpressReceiver
asExpressSignatureVerifier
and modified it a litle. Also, I changed the body parserparseBody
inExpressReceiver
to handlerawBody
. 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 tourl_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[ ]
)