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

Add to Slack #11

Merged
merged 4 commits into from Jul 8, 2019
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -19,6 +19,16 @@
"methods": ["GET"],
"dest": "src/routes/index.js"
},
{
"src": "/slacker",
"methods": ["POST"],
"dest": "src/routes/slacker.js"
},
{
"src": "/slack/oauth",
"methods": ["GET"],
"dest": "src/routes/slacker-oauth.js"
},
{
"src": "/tweeter",
"methods": ["POST"],
@@ -35,6 +45,9 @@
"ROLLBAR_ACCESS_TOKEN": "@pp-rollbar-access-token",
"PASSPLUM_TWEETER_SECRET": "@pp-tweeter-secret",
"PASSPLUM_VOCAB_ID": "@pp-vocab-id",
"SLACK_CLIENT_ID": "@pp-slack-client-id",
"SLACK_CLIENT_SECRET": "@pp-slack-client-secret",
"SLACK_SIGNING_SECRET": "@pp-slack-signing-secret",
"TWITTER_CONSUMER_KEY": "@pp-twitter-consumer-key",
"TWITTER_CONSUMER_SECRET": "@pp-twitter-consumer-secret",
"TWITTER_ACCESS_TOKEN": "@pp-twitter-access-token",

Some generated files are not rendered by default. Learn more.

@@ -3,6 +3,9 @@
"version": "2.1.0",
"description": "Easier, Better Passwords",
"private": true,
"engines": {
"node": "10.x"
},
"repository": {
"type": "git",
"url": "git://github.com/maxbeatty/passplum.git"
@@ -17,6 +20,7 @@
"request": "^2.88.0",
"request-promise-native": "^1.0.7",
"rollbar": "^2.8.1",
"tsscmp": "^1.0.6",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
@@ -7,9 +7,9 @@ const rollbar = new Rollbar({
captureUnhandledRejections: true
});

export function captureError(err /*: Error */) {
export function captureError(err /*: Error */, req /*: $FlowFixMe */) {
console.error(err);
rollbar.error(err);
rollbar.error(err, req);

// wait for serverless
return new Promise(resolve => {
@@ -35,10 +35,10 @@ export class Vault {
}

async fetch(
maxTries /*: number */,
howLong /*: number */,
sep /*: string */,
scoreThreshold /*: number */
howLong /*: number */ = 4,
sep /*: string */ = "-",
maxTries /*: number */ = 10,
scoreThreshold /*: number */ = 4 // Zxcvbn maximum
) /*: Promise<string> */ {
if (maxTries === 0) {
throw new Error("Unable to generate strong passphrase");
@@ -67,7 +67,7 @@ export class Vault {
return phrase;
} catch (error) {
console.error(error);
return this.fetch(--maxTries, howLong, sep, scoreThreshold);
return this.fetch(howLong, sep, --maxTries, scoreThreshold);
}
}
}
@@ -3,7 +3,6 @@
const { captureError } = require("../lib/errors");
const { Vault } = require("../lib/vault");

const MAX_TRIES = 10;
const MAX_LEN = 7;

const v = new Vault();
@@ -26,15 +25,8 @@ module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
}
}

const SCORE_THRESHOLD = 4; // Zxcvbn maximum

await v.load();
const passphrase = await v.fetch(
MAX_TRIES,
LENGTH,
SEPARATOR,
SCORE_THRESHOLD
);
const passphrase = await v.fetch(LENGTH, SEPARATOR);
const permutations = v.getPermutations(LENGTH);

res.send(`
@@ -44,6 +36,7 @@ module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Easier, Better Passwords" />
<meta name="slack-app-id" content="AL6GW8MNY" />
<title>Pass Plum</title>
@@ -403,6 +396,10 @@ module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
</fieldset>
</form>
<a class="db u-center" style="margin-top: 1rem;margin-bottom: -1rem;" href="https://slack.com/oauth/authorize?scope=commands&client_id=3648710574.686574293780">
<img alt="Add to Slack" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" />
</a>
<article class="serif">
<section>
<h2>Why?</h2>
@@ -422,12 +419,8 @@ module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
<section>
<p>
Pass Plum is
<a href="https://github.com/maxbeatty/passplum">open source</a> and
designed so you can easily use your own collection of words.
</p>
<p>
Send your improvements, questions, and ideas to
<a href="https://github.com/maxbeatty/passplum">open source</a>.
Please send your questions and ideas to
<a href="https://github.com/maxbeatty/passplum/issues">GitHub</a> or
<a href="https://twitter.com/passplum">Twitter</a>.
</p>
@@ -458,7 +451,7 @@ module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
</html>
`);
} catch (err) {
await captureError(err);
await captureError(err, req);

res.status(500);
res.send("Internal Server Error");
@@ -0,0 +1,52 @@
// @flow

const { post } = require("request-promise-native");

const { captureError } = require("../lib/errors");

const clientId = process.env.SLACK_CLIENT_ID;
const clientSecret = process.env.SLACK_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error("SLACK_CLIENT_X environment variable missing");
}
const authorization =
"Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");

module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
try {
if (req.query.error === "access_denied") {
res.writeHead(302, "Redirect", { Location: "/" });
res.end();
return;
}

const oauthAccess = await post({
url: "https://slack.com/api/oauth.access",
headers: {
authorization
},
form: {
code: req.query.code
}
});

const { access_token } = JSON.parse(oauthAccess);

const { url: slackUrl } = await post({
url: "https://slack.com/api/auth.test",

headers: {
authorization: `Bearer ${access_token}`
},
json: true
});

res.writeHead(302, "Redirect", { Location: slackUrl });
res.end();
} catch (err) {
await captureError(err, req);

res.status(500);
res.send("Internal Server Error");
}
};
@@ -0,0 +1,83 @@
// @flow

const crypto = require("crypto");
const querystring = require("querystring");
const timingSafeCompare = require("tsscmp");

const { captureError } = require("../lib/errors");
const { Vault } = require("../lib/vault");

const SLACK_SIGNING_SECRET = process.env.SLACK_SIGNING_SECRET;
if (!SLACK_SIGNING_SECRET) {
throw new Error("SLACK_SIGNING_SECRET");
}

const v = new Vault();

// similar to https://github.com/slackapi/node-slack-sdk/blob/a76b3ee5b3e77e6520889b9026a157996d35b84a/packages/interactive-messages/src/http-handler.js#L63
function verifyRequest(reqHeaders, reqBody) {
// Divide current date to match Slack ts format
// Subtract 5 minutes from current time
const fiveMinutesAgo = Math.floor(Date.now() / 1000) - 60 * 5;
const ts = reqHeaders["x-slack-request-timestamp"];
if (ts < fiveMinutesAgo) {
throw new Error("Ignoring request older than 5 minutes from local time");
}

const hmac = crypto.createHmac("sha256", SLACK_SIGNING_SECRET);
const [version, hash] = reqHeaders["x-slack-signature"].split("=");
// undo @now/node helper
const rawBody = querystring.stringify(reqBody);
hmac.update(`${version}:${ts}:${rawBody}`);

if (!timingSafeCompare(hash, hmac.digest("hex"))) {
throw new Error("Slack request signing verification failed");
}
}

module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
try {
verifyRequest(req.headers, req.body);

// Slack verifying SSL ==> respond 200
if (req.body.ssl_check === 1) {
res.send("OK");
return;
}

if (req.body.command !== "/passplum") {
throw new Error("Unknown Slack slash command:" + req.body.command);
}

// could use req.body.text to accept parameters for custom length and separators

if (req.body.text.trim() === "help") {
res.json({
response_type: "ephemeral",
text: "How to use `/passplum`",
attachments: [
{
text:
"Use `/passplum` to generate a :new: unique, _private_ passphrase made of simple, easy-to-type words"
}
]
});
return;
}

await v.load();
const passphrase = await v.fetch();

res.json({
response_type: "ephemeral",
text: "Here's a great password: `" + passphrase + "`"
});
} catch (err) {
await captureError(err, req);

res.json({
response_type: "ephemeral",
text: "Sorry, that didn't work. Please try again."
});
}
};
@@ -1,15 +1,13 @@
// @flow

const assert = require("assert");
const { get, post } = require("request-promise-native");
const { post } = require("request-promise-native");

const { captureError } = require("../lib/errors");
const { Vault } = require("../lib/vault");

assert(
process.env.PASSPLUM_TWEETER_SECRET,
"PASSPLUM_TWEETER_SECRET env var missing"
);
// $FlowFixMe - asserted existence ^^
if (!process.env.PASSPLUM_TWEETER_SECRET) {
throw new Error("PASSPLUM_TWEETER_SECRET env var missing");
}
const secret = `Bearer ${process.env.PASSPLUM_TWEETER_SECRET}`;

const oauth = {
@@ -19,7 +17,7 @@ const oauth = {
token_secret: process.env.TWITTER_ACCESS_SECRET
};

const RE = /<code\s.+?>(.+?)<\/code>/g;
const v = new Vault();

module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
if (req.headers.authorization !== secret) {
@@ -29,27 +27,20 @@ module.exports = async (req /*: $FlowFixMe */, res /*: $FlowFixMe */) => {
}

try {
const body = await get("https://passplum.com");
const matches = RE.exec(body);

if (!matches || matches.length < 2) {
const err = new Error("could not find passphrase");
// $FlowFixMe - I do what I want
err.body = body;
throw err;
}
await v.load();
const passphrase = await v.fetch();

await post({
url: "https://api.twitter.com/1.1/statuses/update.json",
oauth,
qs: {
status: "Here is a great password: " + matches[1]
status: "Here is a great password: " + passphrase
}
});

res.json({ status: "success" });
} catch (err) {
await captureError(err);
await captureError(err, req);

res.status(500);
res.send("Internal Server Error");
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.