Skip to content

Commit

Permalink
Implement reCAPTCHA for abuse prevention (#311)
Browse files Browse the repository at this point in the history
* Implement reCAPTCHA for abuse prevention

* Update the README
  • Loading branch information
chadwhitacre authored and rauchg committed Jun 1, 2017
1 parent 3ff77b9 commit 1dedb2e
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 21 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ RUN npm install --unsafe-perm


EXPOSE 3000 EXPOSE 3000


CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY
2 changes: 1 addition & 1 deletion Procfile
Original file line number Original file line Diff line number Diff line change
@@ -1 +1 @@
web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY
10 changes: 9 additions & 1 deletion README.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- A landing page you can point users to fill in their emails and receive an invite (`https://slack.yourdomain.com`) - A landing page you can point users to fill in their emails and receive an invite (`https://slack.yourdomain.com`)
- An `<iframe>` badge to embed on any website that shows connected users in *realtime* with socket.io. - An `<iframe>` badge to embed on any website that shows connected users in *realtime* with socket.io.
- A SVG badge that works well from static mediums (like GitHub README pages) - A SVG badge that works well from static mediums (like GitHub README pages)
- Abuse prevention via [Google reCAPTCHA](https://www.google.com/recaptcha/intro/)


Check out the [Demo](https://slackin.now.sh/) or read more about the [motivations and history](http://rauchg.com/slackin) behind Slackin. Check out the [Demo](https://slackin.now.sh/) or read more about the [motivations and history](http://rauchg.com/slackin) behind Slackin.


Expand All @@ -13,7 +14,11 @@ Check out the [Demo](https://slackin.now.sh/) or read more about the [motivation
Set up [Now](https://zeit.co/now) on your device and run this command: Set up [Now](https://zeit.co/now) on your device and run this command:


```bash ```bash
$ now -e SLACK_API_TOKEN="<token>" -e SLACK_SUBDOMAIN="<team-name>" now-examples/slackin $ now -e SLACK_API_TOKEN="<token>" \
-e SLACK_SUBDOMAIN="<team-name>" \
-e GOOGLE_CAPTCHA_SECRET="<secret>" \
-e GOOGLE_CAPTCHA_SITEKEY="<sitekey>" \
now-examples/slackin
``` ```


Other platforms: Other platforms:
Expand All @@ -35,6 +40,9 @@ times 5. If you are not getting invite emails, this might be the reason.
Workaround: sign up for a free org, and set up Slackin to point to it Workaround: sign up for a free org, and set up Slackin to point to it
(all channels will be visible). (all channels will be visible).


Here is where to [generate a secret and
sitekey](https://www.google.com/recaptcha/admin) for Google reCAPTCHA.

### Badges ### Badges


#### Realtime ([demo](https://cldup.com/IaiPnDEAA6.gif)) #### Realtime ([demo](https://cldup.com/IaiPnDEAA6.gif))
Expand Down
8 changes: 8 additions & 0 deletions app.json
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@
"description": "A Slack API token (find it on https://api.slack.com/web)", "description": "A Slack API token (find it on https://api.slack.com/web)",
"required": true "required": true
}, },
"GOOGLE_CAPTCHA_SECRET": {
"description": "Google captcha secret key",
"required": true
},
"GOOGLE_CAPTCHA_SITEKEY": {
"description": "Google captcha site key",
"required": true
},
"SLACK_COC": { "SLACK_COC": {
"description": "A URL to a Code of Conduct people must agree on before joining.", "description": "A URL to a Code of Conduct people must agree on before joining.",
"required": false "required": false
Expand Down
11 changes: 9 additions & 2 deletions bin/slackin
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -17,24 +17,31 @@ args
.option(['?', 'help'], 'Show the usage information') .option(['?', 'help'], 'Show the usage information')


var flags = args.parse(process.argv, { var flags = args.parse(process.argv, {
value: '<team-id> <api-token>', value: '<team-id> <api-token> <google-captcha-secret> <google-captcha-sitekey>',
help: false help: false
}) })


var org = args.sub[0] || process.env.SLACK_SUBDOMAIN var org = args.sub[0] || process.env.SLACK_SUBDOMAIN
var token = args.sub[1] || process.env.SLACK_API_TOKEN var token = args.sub[1] || process.env.SLACK_API_TOKEN
var emails = process.env.EMAIL_SLACK_LIST || '' var emails = process.env.EMAIL_SLACK_LIST || ''


var gcaptcha_secret = args.sub[2] || process.env.GOOGLE_CAPTCHA_SECRET
var gcaptcha_sitekey = args.sub[3] || process.env.GOOGLE_CAPTCHA_SITEKEY



if (flags.help) { if (flags.help) {
args.showHelp() args.showHelp()
} }


if (!org || !token) { if (!org || !token || !gcaptcha_sitekey || !gcaptcha_secret) {
args.showHelp() args.showHelp()
} else { } else {
flags.org = org flags.org = org
flags.token = token flags.token = token
flags.emails = emails flags.emails = emails
flags.gcaptcha_secret = gcaptcha_secret
flags.gcaptcha_sitekey = gcaptcha_sitekey
} }


var port = flags.port var port = flags.port
Expand Down
6 changes: 4 additions & 2 deletions lib/assets/client.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ body.addEventListener('submit', function (ev){
button.disabled = true button.disabled = true
button.className = '' button.className = ''
button.innerHTML = 'Please Wait' button.innerHTML = 'Please Wait'
invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, function (err, msg){ var gcaptcha_response = form.elements['g-recaptcha-response']
invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, gcaptcha_response.value, function (err, msg){
if (err) { if (err) {
button.removeAttribute('disabled') button.removeAttribute('disabled')
button.className = 'error' button.className = 'error'
Expand All @@ -31,10 +32,11 @@ body.addEventListener('submit', function (ev){
}) })
}) })


function invite (channel, coc, email, fn){ function invite (channel, coc, email, gcaptcha_response_value, fn){
request request
.post(data.path + 'invite') .post(data.path + 'invite')
.send({ .send({
"g-recaptcha-response": gcaptcha_response_value,
coc: coc, coc: coc,
channel: channel, channel: channel,
email: email email: email
Expand Down
83 changes: 70 additions & 13 deletions lib/index.js
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Server as http } from 'http'
import remail from 'email-regex' import remail from 'email-regex'
import dom from 'vd' import dom from 'vd'
import cors from 'cors' import cors from 'cors'
import request from 'superagent';


// our code // our code
import Slack from './slack' import Slack from './slack'
Expand All @@ -22,6 +23,8 @@ export default function slackin ({
token, token,
interval = 5000, // jshint ignore:line interval = 5000, // jshint ignore:line
org, org,
gcaptcha_secret,
gcaptcha_sitekey,
css, css,
coc, coc,
cors: useCors = false, cors: useCors = false,
Expand All @@ -33,6 +36,8 @@ export default function slackin ({
// must haves // must haves
if (!token) throw new Error('Must provide a `token`.') if (!token) throw new Error('Must provide a `token`.')
if (!org) throw new Error('Must provide an `org`.') if (!org) throw new Error('Must provide an `org`.')
if (!gcaptcha_secret) throw new Error('Must provide a `gcaptcha_secret`.')
if (!gcaptcha_sitekey) throw new Error('Must provide an `gcaptcha_sitekey`.')


if (channels) { if (channels) {
// convert to an array // convert to an array
Expand Down Expand Up @@ -84,11 +89,12 @@ export default function slackin ({
dom('title', dom('title',
'Join ', name, ' on Slack!' 'Join ', name, ' on Slack!'
), ),
dom("script src=https://www.google.com/recaptcha/api.js"),
dom('meta name=viewport content="width=device-width,initial-scale=1.0,minimum-scale=1.0,user-scalable=no"'), dom('meta name=viewport content="width=device-width,initial-scale=1.0,minimum-scale=1.0,user-scalable=no"'),
dom('link rel="shortcut icon" href=https://slack.global.ssl.fastly.net/272a/img/icons/favicon-32.png'), dom('link rel="shortcut icon" href=https://slack.global.ssl.fastly.net/272a/img/icons/favicon-32.png'),
css && dom('link rel=stylesheet', { href: css }) css && dom('link rel=stylesheet', { href: css })
), ),
splash({ coc, path, css, name, org, logo, channels, active, total }) splash({ coc, path, css, name, org, logo, channels, active, total, gcaptcha_sitekey})
) )
res.type('html') res.type('html')
res.send(page.toHTML()) res.send(page.toHTML())
Expand Down Expand Up @@ -130,13 +136,20 @@ export default function slackin ({
} }


let email = req.body.email let email = req.body.email
let captcha_response = req.body['g-recaptcha-response'];


if (!email) { if (!email) {
return res return res
.status(400) .status(400)
.json({ msg: 'No email provided' }) .json({ msg: 'No email provided' })
} }


if(captcha_response == undefined || !captcha_response.length){
return res
.status(400)
.send({ msg: 'Invalid captcha' });
}

if (!remail().test(email)) { if (!remail().test(email)) {
return res return res
.status(400) .status(400)
Expand All @@ -156,23 +169,67 @@ export default function slackin ({
.json({ msg: 'Agreement to CoC is mandatory' }) .json({ msg: 'Agreement to CoC is mandatory' })
} }


invite({ token, org, email, channel: chanId }, err => { /////////////////////////////////////////////////////////////////////////
if (err) {
if (err.message === `Sending you to Slack...`) {
return res
.status(303)
.json({ msg: err.message, redirectUrl: `https://${org}.slack.com` })
}


const captcha_data = {
secret: gcaptcha_secret,
response: captcha_response,
remoteip: req.connection.remoteAddress
}


const captcha_callback = (err, resp) => {

if (err) {
return res return res
.status(400) .status(400)
.json({ msg: err.message }) .send({ msg: err });

}else{

if(resp.body.success){

let chanId = slack.channel ? slack.channel.id : null;

invite({ token, org, email, channel: chanId }, err => {
if (err) {
if (err.message === `Sending you to Slack...`) {
return res
.status(303)
.json({ msg: err.message, redirectUrl: `https://${org}.slack.com` })
}

return res
.status(400)
.json({ msg: err.message })
}

res
.status(200)
.json({ msg: 'WOOT. Check your email!' })
});

}else{

if (err) {
return res
.status(400)
.send({ msg: "Captcha check failed" });
}
}

} }


res }
.status(200)
.json({ msg: 'WOOT. Check your email!' })
}) request.post('https://www.google.com/recaptcha/api/siteverify')
.type('form')
.send(captcha_data)
.end(captcha_callback);


}) })


// iframe // iframe
Expand Down
4 changes: 3 additions & 1 deletion lib/splash.js
Original file line number Original file line Diff line number Diff line change
@@ -1,6 +1,6 @@
import dom from 'vd' import dom from 'vd'


export default function splash ({ path, name, org, coc, logo, active, total, channels, large, iframe }){ export default function splash ({ path, name, org, coc, logo, active, total, channels, large, iframe, gcaptcha_sitekey }){
let div = dom('.splash', let div = dom('.splash',
!iframe && dom('.logos', !iframe && dom('.logos',
logo && dom('.logo.org'), logo && dom('.logo.org'),
Expand Down Expand Up @@ -34,6 +34,8 @@ export default function splash ({ path, name, org, coc, logo, active, total, cha
), ),
dom('input.form-item type=email name=email placeholder=you@yourdomain.com ' dom('input.form-item type=email name=email placeholder=you@yourdomain.com '
+ (!iframe ? 'autofocus' : '')), + (!iframe ? 'autofocus' : '')),
dom('br'),
dom(`div class="g-recaptcha" data-sitekey="${gcaptcha_sitekey}"`),
coc && dom('.coc', coc && dom('.coc',
dom('label', dom('label',
dom('input type=checkbox name=coc value=1'), dom('input type=checkbox name=coc value=1'),
Expand Down

0 comments on commit 1dedb2e

Please sign in to comment.