Skip to content

Commit 1dedb2e

Browse files
chadwhitacrerauchg
authored andcommitted
Implement reCAPTCHA for abuse prevention (#311)
* Implement reCAPTCHA for abuse prevention * Update the README
1 parent 3ff77b9 commit 1dedb2e

File tree

8 files changed

+105
-21
lines changed

8 files changed

+105
-21
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ RUN npm install --unsafe-perm
1010

1111
EXPOSE 3000
1212

13-
CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN
13+
CMD ./bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY

Procfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN
1+
web: bin/slackin --coc "$SLACK_COC" --channels "$SLACK_CHANNELS" --port $PORT $SLACK_SUBDOMAIN $SLACK_API_TOKEN $GOOGLE_CAPTCHA_SECRET $GOOGLE_CAPTCHA_SITEKEY

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
- A landing page you can point users to fill in their emails and receive an invite (`https://slack.yourdomain.com`)
66
- An `<iframe>` badge to embed on any website that shows connected users in *realtime* with socket.io.
77
- A SVG badge that works well from static mediums (like GitHub README pages)
8+
- Abuse prevention via [Google reCAPTCHA](https://www.google.com/recaptcha/intro/)
89

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

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

1516
```bash
16-
$ now -e SLACK_API_TOKEN="<token>" -e SLACK_SUBDOMAIN="<team-name>" now-examples/slackin
17+
$ now -e SLACK_API_TOKEN="<token>" \
18+
-e SLACK_SUBDOMAIN="<team-name>" \
19+
-e GOOGLE_CAPTCHA_SECRET="<secret>" \
20+
-e GOOGLE_CAPTCHA_SITEKEY="<sitekey>" \
21+
now-examples/slackin
1722
```
1823

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

43+
Here is where to [generate a secret and
44+
sitekey](https://www.google.com/recaptcha/admin) for Google reCAPTCHA.
45+
3846
### Badges
3947

4048
#### Realtime ([demo](https://cldup.com/IaiPnDEAA6.gif))

app.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
"description": "A Slack API token (find it on https://api.slack.com/web)",
1414
"required": true
1515
},
16+
"GOOGLE_CAPTCHA_SECRET": {
17+
"description": "Google captcha secret key",
18+
"required": true
19+
},
20+
"GOOGLE_CAPTCHA_SITEKEY": {
21+
"description": "Google captcha site key",
22+
"required": true
23+
},
1624
"SLACK_COC": {
1725
"description": "A URL to a Code of Conduct people must agree on before joining.",
1826
"required": false

bin/slackin

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,31 @@ args
1717
.option(['?', 'help'], 'Show the usage information')
1818

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

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

28+
var gcaptcha_secret = args.sub[2] || process.env.GOOGLE_CAPTCHA_SECRET
29+
var gcaptcha_sitekey = args.sub[3] || process.env.GOOGLE_CAPTCHA_SITEKEY
30+
31+
32+
2833
if (flags.help) {
2934
args.showHelp()
3035
}
3136

32-
if (!org || !token) {
37+
if (!org || !token || !gcaptcha_sitekey || !gcaptcha_secret) {
3338
args.showHelp()
3439
} else {
3540
flags.org = org
3641
flags.token = token
3742
flags.emails = emails
43+
flags.gcaptcha_secret = gcaptcha_secret
44+
flags.gcaptcha_sitekey = gcaptcha_sitekey
3845
}
3946

4047
var port = flags.port

lib/assets/client.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ body.addEventListener('submit', function (ev){
1919
button.disabled = true
2020
button.className = ''
2121
button.innerHTML = 'Please Wait'
22-
invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, function (err, msg){
22+
var gcaptcha_response = form.elements['g-recaptcha-response']
23+
invite(channel ? channel.value : null, coc && coc.checked ? 1 : 0, email.value, gcaptcha_response.value, function (err, msg){
2324
if (err) {
2425
button.removeAttribute('disabled')
2526
button.className = 'error'
@@ -31,10 +32,11 @@ body.addEventListener('submit', function (ev){
3132
})
3233
})
3334

34-
function invite (channel, coc, email, fn){
35+
function invite (channel, coc, email, gcaptcha_response_value, fn){
3536
request
3637
.post(data.path + 'invite')
3738
.send({
39+
"g-recaptcha-response": gcaptcha_response_value,
3840
coc: coc,
3941
channel: channel,
4042
email: email

lib/index.js

Lines changed: 70 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Server as http } from 'http'
99
import remail from 'email-regex'
1010
import dom from 'vd'
1111
import cors from 'cors'
12+
import request from 'superagent';
1213

1314
// our code
1415
import Slack from './slack'
@@ -22,6 +23,8 @@ export default function slackin ({
2223
token,
2324
interval = 5000, // jshint ignore:line
2425
org,
26+
gcaptcha_secret,
27+
gcaptcha_sitekey,
2528
css,
2629
coc,
2730
cors: useCors = false,
@@ -33,6 +36,8 @@ export default function slackin ({
3336
// must haves
3437
if (!token) throw new Error('Must provide a `token`.')
3538
if (!org) throw new Error('Must provide an `org`.')
39+
if (!gcaptcha_secret) throw new Error('Must provide a `gcaptcha_secret`.')
40+
if (!gcaptcha_sitekey) throw new Error('Must provide an `gcaptcha_sitekey`.')
3641

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

132138
let email = req.body.email
139+
let captcha_response = req.body['g-recaptcha-response'];
133140

134141
if (!email) {
135142
return res
136143
.status(400)
137144
.json({ msg: 'No email provided' })
138145
}
139146

147+
if(captcha_response == undefined || !captcha_response.length){
148+
return res
149+
.status(400)
150+
.send({ msg: 'Invalid captcha' });
151+
}
152+
140153
if (!remail().test(email)) {
141154
return res
142155
.status(400)
@@ -156,23 +169,67 @@ export default function slackin ({
156169
.json({ msg: 'Agreement to CoC is mandatory' })
157170
}
158171

159-
invite({ token, org, email, channel: chanId }, err => {
160-
if (err) {
161-
if (err.message === `Sending you to Slack...`) {
162-
return res
163-
.status(303)
164-
.json({ msg: err.message, redirectUrl: `https://${org}.slack.com` })
165-
}
172+
/////////////////////////////////////////////////////////////////////////
173+
166174

175+
const captcha_data = {
176+
secret: gcaptcha_secret,
177+
response: captcha_response,
178+
remoteip: req.connection.remoteAddress
179+
}
180+
181+
182+
const captcha_callback = (err, resp) => {
183+
184+
if (err) {
167185
return res
168186
.status(400)
169-
.json({ msg: err.message })
187+
.send({ msg: err });
188+
189+
}else{
190+
191+
if(resp.body.success){
192+
193+
let chanId = slack.channel ? slack.channel.id : null;
194+
195+
invite({ token, org, email, channel: chanId }, err => {
196+
if (err) {
197+
if (err.message === `Sending you to Slack...`) {
198+
return res
199+
.status(303)
200+
.json({ msg: err.message, redirectUrl: `https://${org}.slack.com` })
201+
}
202+
203+
return res
204+
.status(400)
205+
.json({ msg: err.message })
206+
}
207+
208+
res
209+
.status(200)
210+
.json({ msg: 'WOOT. Check your email!' })
211+
});
212+
213+
}else{
214+
215+
if (err) {
216+
return res
217+
.status(400)
218+
.send({ msg: "Captcha check failed" });
219+
}
220+
}
221+
170222
}
171223

172-
res
173-
.status(200)
174-
.json({ msg: 'WOOT. Check your email!' })
175-
})
224+
}
225+
226+
227+
request.post('https://www.google.com/recaptcha/api/siteverify')
228+
.type('form')
229+
.send(captcha_data)
230+
.end(captcha_callback);
231+
232+
176233
})
177234

178235
// iframe

lib/splash.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import dom from 'vd'
22

3-
export default function splash ({ path, name, org, coc, logo, active, total, channels, large, iframe }){
3+
export default function splash ({ path, name, org, coc, logo, active, total, channels, large, iframe, gcaptcha_sitekey }){
44
let div = dom('.splash',
55
!iframe && dom('.logos',
66
logo && dom('.logo.org'),
@@ -34,6 +34,8 @@ export default function splash ({ path, name, org, coc, logo, active, total, cha
3434
),
3535
dom('input.form-item type=email name=email [email protected] '
3636
+ (!iframe ? 'autofocus' : '')),
37+
dom('br'),
38+
dom(`div class="g-recaptcha" data-sitekey="${gcaptcha_sitekey}"`),
3739
coc && dom('.coc',
3840
dom('label',
3941
dom('input type=checkbox name=coc value=1'),

0 commit comments

Comments
 (0)