如何验证来自 Slack Events API 的请求
How to verify a request from Slack Events API
我正在使用 validate-slack-request 包来验证我传入的 slack 请求是否来自 slack。这适用于 Slash 命令和交互式组件(按钮等)。但是它不适用于事件 API
我注意到 POST 请求正文的事件格式不同 API。没有payload
。但我不清楚 slack 给我用来验证的是什么。代码如下
//This WORKS
app.post("/interactiveCommand", async (req, res) => {
const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
if (!legit) {
console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
return res.status(403).send("Unauthorized");
}
await interactiveCommand(...);
return;
});
//This does NOT WORK
app.post("/slackEvents", parser, json, async (req, res) => {
const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
if (!legit) {
console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
res.status(403).send("Unauthorized");
} else {
try {
switch (req.body.event.type) {
case "message":
await handleMessageEvent(...);
break;
case "app_home_opened":
res.status(200).send();
await updateUserHomePage(...);
break;
default:
res.status(200).send();
return;
}
} catch(e) {
console.log("Error with event handling! ", e);
}
}
});
const crypto = require('crypto')
const querystring = require('querystring')
// Adhering to RFC 3986
// Inspired from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function fixedEncodeURIComponent (str) {
return str.replace(/[!'()*~]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
/**
* Validate incoming Slack request
*
* @param {string} slackAppSigningSecret - Slack application signing secret
* @param {object} httpReq - Express request object
* @param {boolean} [logging=false] - Enable logging to console
*
* @returns {boolean} Result of vlaidation
*/
function validateSlackRequest (slackAppSigningSecret, httpReq, logging) {
logging = logging || false
if (typeof logging !== 'boolean') {
throw new Error('Invalid type for logging. Provided ' + typeof logging + ', expected boolean')
}
if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') {
throw new Error('Invalid slack app signing secret')
}
const xSlackRequestTimeStamp = httpReq.get('X-Slack-Request-Timestamp')
const SlackSignature = httpReq.get('X-Slack-Signature')
const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1
if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
if (logging) { console.log('Missing part in Slack\'s request') }
return false
}
const baseString = 'v0:' + xSlackRequestTimeStamp + ':' + bodyPayload
const hash = 'v0=' + crypto.createHmac('sha256', slackAppSigningSecret)
.update(baseString)
.digest('hex')
if (logging) {
console.log('Slack verifcation:\n Request body: ' + bodyPayload + '\n Calculated Hash: ' + hash + '\n Slack-Signature: ' + SlackSignature)
}
return (SlackSignature === hash)
}
我将文本有效负载转换为 json,方法是:
IFS=$'\n'
if [ "$REQUEST_METHOD" = "POST" ]; then
if [ "$CONTENT_LENGTH" -gt 0 ]; then
cat - > /tmp/file.txt
fi
fi
cat /tmp/file.txt | sed -e "s/^payload=//g" | perl -pe 's/\%(\w\w)/chr hex /ge' > /tmp/file.json
这就是我让它工作的方式,这是一些反复试验,我不做任何承诺。基本上,如果我正在验证事件而不是斜线命令或交互式组件,我会将 type="Event"
传递给验证函数。唯一的变化是我如何根据传入请求构建有效负载
export function validateSlackRequest(
slackAppSigningSecret,
httpReq,
logging,
type = ""
) {
logging = logging || false;
if (typeof logging !== "boolean") {
throw new Error(
"Invalid type for logging. Provided " +
typeof logging +
", expected boolean"
);
}
if (
!slackAppSigningSecret ||
typeof slackAppSigningSecret !== "string" ||
slackAppSigningSecret === ""
) {
throw new Error("Invalid slack app signing secret");
}
const xSlackRequestTimeStamp = httpReq.get("X-Slack-Request-Timestamp");
const SlackSignature = httpReq.get("X-Slack-Signature");
let bodyPayload;
if (type === "Event") {
bodyPayload = (httpReq as any).rawBody;
} else {
bodyPayload = fixedEncodeURIComponent(
querystring.stringify(httpReq.body).replace(/%20/g, "+")
); // Fix for #1
}
if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
if (logging) {
console.log("Missing part in Slack's request");
}
return false;
}
const baseString = "v0:" + xSlackRequestTimeStamp + ":" + bodyPayload;
const hash =
"v0=" +
crypto
.createHmac("sha256", slackAppSigningSecret)
.update(baseString)
.digest("hex");
if (logging) {
console.log(
"Slack verification:\nTimestamp: " +
xSlackRequestTimeStamp +
"\n Request body: " +
bodyPayload +
"\n Calculated Hash: " +
hash +
"\n Slack-Signature: " +
SlackSignature
);
}
return SlackSignature === hash;
}
我正在使用 validate-slack-request 包来验证我传入的 slack 请求是否来自 slack。这适用于 Slash 命令和交互式组件(按钮等)。但是它不适用于事件 API
我注意到 POST 请求正文的事件格式不同 API。没有payload
。但我不清楚 slack 给我用来验证的是什么。代码如下
//This WORKS
app.post("/interactiveCommand", async (req, res) => {
const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
if (!legit) {
console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
return res.status(403).send("Unauthorized");
}
await interactiveCommand(...);
return;
});
//This does NOT WORK
app.post("/slackEvents", parser, json, async (req, res) => {
const legit = validateSlackRequest(process.env.SLACK_SIGNING_SECRET, req, false);
if (!legit) {
console.log("UNAUTHORIZED ACCESS ", req.headers, req.body);
res.status(403).send("Unauthorized");
} else {
try {
switch (req.body.event.type) {
case "message":
await handleMessageEvent(...);
break;
case "app_home_opened":
res.status(200).send();
await updateUserHomePage(...);
break;
default:
res.status(200).send();
return;
}
} catch(e) {
console.log("Error with event handling! ", e);
}
}
});
const crypto = require('crypto')
const querystring = require('querystring')
// Adhering to RFC 3986
// Inspired from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function fixedEncodeURIComponent (str) {
return str.replace(/[!'()*~]/g, function (c) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase()
})
}
/**
* Validate incoming Slack request
*
* @param {string} slackAppSigningSecret - Slack application signing secret
* @param {object} httpReq - Express request object
* @param {boolean} [logging=false] - Enable logging to console
*
* @returns {boolean} Result of vlaidation
*/
function validateSlackRequest (slackAppSigningSecret, httpReq, logging) {
logging = logging || false
if (typeof logging !== 'boolean') {
throw new Error('Invalid type for logging. Provided ' + typeof logging + ', expected boolean')
}
if (!slackAppSigningSecret || typeof slackAppSigningSecret !== 'string' || slackAppSigningSecret === '') {
throw new Error('Invalid slack app signing secret')
}
const xSlackRequestTimeStamp = httpReq.get('X-Slack-Request-Timestamp')
const SlackSignature = httpReq.get('X-Slack-Signature')
const bodyPayload = fixedEncodeURIComponent(querystring.stringify(httpReq.body).replace(/%20/g, '+')) // Fix for #1
if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
if (logging) { console.log('Missing part in Slack\'s request') }
return false
}
const baseString = 'v0:' + xSlackRequestTimeStamp + ':' + bodyPayload
const hash = 'v0=' + crypto.createHmac('sha256', slackAppSigningSecret)
.update(baseString)
.digest('hex')
if (logging) {
console.log('Slack verifcation:\n Request body: ' + bodyPayload + '\n Calculated Hash: ' + hash + '\n Slack-Signature: ' + SlackSignature)
}
return (SlackSignature === hash)
}
我将文本有效负载转换为 json,方法是:
IFS=$'\n'
if [ "$REQUEST_METHOD" = "POST" ]; then
if [ "$CONTENT_LENGTH" -gt 0 ]; then
cat - > /tmp/file.txt
fi
fi
cat /tmp/file.txt | sed -e "s/^payload=//g" | perl -pe 's/\%(\w\w)/chr hex /ge' > /tmp/file.json
这就是我让它工作的方式,这是一些反复试验,我不做任何承诺。基本上,如果我正在验证事件而不是斜线命令或交互式组件,我会将 type="Event"
传递给验证函数。唯一的变化是我如何根据传入请求构建有效负载
export function validateSlackRequest(
slackAppSigningSecret,
httpReq,
logging,
type = ""
) {
logging = logging || false;
if (typeof logging !== "boolean") {
throw new Error(
"Invalid type for logging. Provided " +
typeof logging +
", expected boolean"
);
}
if (
!slackAppSigningSecret ||
typeof slackAppSigningSecret !== "string" ||
slackAppSigningSecret === ""
) {
throw new Error("Invalid slack app signing secret");
}
const xSlackRequestTimeStamp = httpReq.get("X-Slack-Request-Timestamp");
const SlackSignature = httpReq.get("X-Slack-Signature");
let bodyPayload;
if (type === "Event") {
bodyPayload = (httpReq as any).rawBody;
} else {
bodyPayload = fixedEncodeURIComponent(
querystring.stringify(httpReq.body).replace(/%20/g, "+")
); // Fix for #1
}
if (!(xSlackRequestTimeStamp && SlackSignature && bodyPayload)) {
if (logging) {
console.log("Missing part in Slack's request");
}
return false;
}
const baseString = "v0:" + xSlackRequestTimeStamp + ":" + bodyPayload;
const hash =
"v0=" +
crypto
.createHmac("sha256", slackAppSigningSecret)
.update(baseString)
.digest("hex");
if (logging) {
console.log(
"Slack verification:\nTimestamp: " +
xSlackRequestTimeStamp +
"\n Request body: " +
bodyPayload +
"\n Calculated Hash: " +
hash +
"\n Slack-Signature: " +
SlackSignature
);
}
return SlackSignature === hash;
}