Skip to content

Commit

Permalink
Merge pull request #24 from zeeshanakram3/bypass_captcha_verification
Browse files Browse the repository at this point in the history
Bypass Captcha Verification for Certain Requests
  • Loading branch information
mnaamani authored Jul 19, 2023
2 parents 36a9e6b + 4cb27b1 commit c4965ed
Show file tree
Hide file tree
Showing 7 changed files with 60 additions and 18 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Will build and start the server.

## API

### `POST /register`
Send a JSON in the request body:
### `POST -H Authorization: Bearer {CAPTCHA_BYPASS_KEY} /register`
Send a JSON in the request body with an optional Bearer Authentication (if request wants to bypass captcha verification):

```
{
Expand Down Expand Up @@ -108,4 +108,4 @@ docker compose up -d
### Setup Reverse proxy
The webserice will be listening on localhost on the port you configured in your .env file.
To make the service available publicly you will need to setup a reverse proxy with an ssl certificate.
Nginx and caddy are good options. Example usage of caddy in [docker/docke-compose stack](https://hub.docker.com/_/caddy)
Nginx and caddy are good options. Example usage of caddy with [docker/docker-compose stack](https://hub.docker.com/_/caddy)
1 change: 1 addition & 0 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ GLOBAL_API_LIMIT_INTERVAL_HOURS=1
GLOBAL_API_LIMIT_MAX_IN_INTERVAL=10
PER_IP_API_LIMIT_INTERVAL_HOURS=48
PER_IP_API_LIMIT_MAX_IN_INTERVAL=1
CAPTCHA_BYPASS_KEY=random-string-key
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@joystream/member-faucet",
"version": "0.1.0",
"version": "0.2.0",
"main": "./lib/app.js",
"license": "MIT",
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ app.post('/register', async (req, res) => {
captchaToken,
} = req.body

const captchaBypassKey = (req: express.Request) => {
const authHeader = req.headers['authorization']
return authHeader && authHeader.split(' ')[1]
}

processingRequest.lock(async () => {
try {
await register(
Expand All @@ -105,6 +110,7 @@ app.post('/register', async (req, res) => {
about,
externalResources,
captchaToken,
captchaBypassKey(req),
callback
)
} catch (err) {
Expand Down
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,7 @@ export const ALERT_TO_EMAIL = process.env.ALERT_TO_EMAIL
export const HCAPTCHA_ENDPOINT = 'https://hcaptcha.com/siteverify'
export const HCAPTCHA_SECRET = process.env.HCAPTCHA_SECRET
export const HCAPTCHA_ENABLED = HCAPTCHA_ENDPOINT && HCAPTCHA_SECRET

// A server-side configured key that client should send (in the `Authorization` header) along with
// the request to `/register` endpoint if they want to bypass the captcha verification requirement.
export const CAPTCHA_BYPASS_KEY = process.env.CAPTCHA_BYPASS_KEY
57 changes: 44 additions & 13 deletions src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MembershipMetadata } from '@joystream/metadata-protobuf'
import IExternalResource = MembershipMetadata.IExternalResource
import {
ENABLE_API_THROTTLING,
CAPTCHA_BYPASS_KEY,
GLOBAL_API_LIMIT_INTERVAL_HOURS,
GLOBAL_API_LIMIT_MAX_IN_INTERVAL,
HCAPTCHA_ENABLED,
Expand All @@ -28,12 +29,18 @@ const globalLimiter = new InMemoryRateLimiter({
maxInInterval: GLOBAL_API_LIMIT_MAX_IN_INTERVAL,
})

// per ip rate limit
// per ip rate limit to apply after input validation checks
const ipLimiter = new InMemoryRateLimiter({
interval: PER_IP_API_LIMIT_INTERVAL_HOURS * 60 * 60 * 1000, // milliseconds
maxInInterval: PER_IP_API_LIMIT_MAX_IN_INTERVAL,
})

// very aggressive ip limit for failed authentication
const authLimiter = new InMemoryRateLimiter({
interval: 1 * 60 * 60 * 1000, // milliseconds
maxInInterval: 3,
})

function memberIdFromEvent(events: EventRecord[]): MemberId | undefined {
return getDataFromEvent(events, 'members', 'MembershipGifted', 0)
}
Expand All @@ -56,31 +63,53 @@ export async function register(
avatar: string | undefined,
about: string,
externalResources: IExternalResource[],
captchaToken: string,
captchaToken: string | undefined,
captchaBypassKey: string | undefined,
callback: RegisterCallback
) {
// verify captcha if enabled
if (HCAPTCHA_ENABLED) {
if (!captchaToken) {

let canBypass = false
// Check if request is authorized to bypass captcha verification and ip rate limits
if ((HCAPTCHA_ENABLED || ENABLE_API_THROTTLING) && captchaBypassKey && CAPTCHA_BYPASS_KEY) {
const wasBlockedIp = await authLimiter.limit(`${ip}-auth`)
if((captchaBypassKey !== CAPTCHA_BYPASS_KEY) || wasBlockedIp) {
callback(
{
error: 'MissingCaptchaToken',
error: 'Unauthorized', // keep it general, no need to reveal if throttle or bad key
},
400
403
)
log(`Too many failed auth attempts from ${ip}`)
return
} else {
authLimiter.clear(`${ip}-auth`)
canBypass = true
}
const captchaResult = await verifyCaptcha(captchaToken)
if (captchaResult !== true) {
log('captcha verification failed')
}

// verify captcha if enabled
if (HCAPTCHA_ENABLED && !canBypass) {
if (!captchaToken) {
callback(
{
error: 'InvalidCaptchaToken',
errorCodes: captchaResult,
error: 'MissingCaptchaToken',
},
400
)
return
} else {
const captchaResult = await verifyCaptcha(captchaToken)
if (captchaResult !== true) {
log('captcha verification failed')
callback(
{
error: 'InvalidCaptchaToken',
errorCodes: captchaResult,
},
400
)
return
}
}
}

Expand Down Expand Up @@ -184,7 +213,9 @@ export async function register(
return callback('FaucetExhausted', 400)
}

if (ENABLE_API_THROTTLING) {
// Do throttling after all input validation checks to avoid DoS attack by someone repeatedly
// trying to register with most likely outcome being unsuccsessful.
if (ENABLE_API_THROTTLING && !canBypass) {
// apply limit per ip address
const wasBlockedIp = await ipLimiter.limit(`${ip}-register`)
if (wasBlockedIp) {
Expand Down

0 comments on commit c4965ed

Please sign in to comment.