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

Phase out automatic CAPTCHA #236

Merged
merged 1 commit into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,6 @@ CDN_ACCESS_TOKEN=SOME_SECRET_TOKEN

# Captcha API
CAPTCHA_SALT=
## We use AES-256-GCM, so the key length MUST BE 32 bytes
CAPTCHA_ENCRYPTION_KEY=

# Google SSO
GOOGLE_OAUTH2_CLIENT_ID=
Expand Down
1 change: 0 additions & 1 deletion Dockerfile.prod
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ ENV TZ=UTC \
CDN_URL=http://localhost:5006 \
CDN_ACCESS_TOKEN=SOME_SECRET_TOKEN \
CAPTCHA_SALT= \
CAPTCHA_ENCRYPTION_KEY= \
GOOGLE_OAUTH2_CLIENT_ID= \
GOOGLE_OAUTH2_CLIENT_SECRET= \
GITHUB_OAUTH2_CLIENT_ID= \
Expand Down
156 changes: 5 additions & 151 deletions apps/production/src/captcha/captcha.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,20 @@ import {
Body,
UseGuards,
ForbiddenException,
InternalServerErrorException,
Headers,
Request,
Req,
Response,
Res,
Ip,
HttpCode,
} from '@nestjs/common'
import * as dayjs from 'dayjs'
import * as utc from 'dayjs/plugin/utc'

import { AppLoggerService } from '../logger/logger.service'
import { CAPTCHA_COOKIE_KEY } from '../common/constants'
import { getIPFromHeaders } from '../common/utils'
import { CaptchaService, DUMMY_PIDS, isDummyPID } from './captcha.service'
import { CaptchaService, DUMMY_PIDS } from './captcha.service'
import { BotDetectionGuard } from '../common/guards/bot-detection.guard'
import { BotDetection } from '../common/decorators/bot-detection.decorator'
import { ManualDTO } from './dtos/manual.dto'
import { ValidateDTO } from './dtos/validate.dto'
import { AutomaticDTO } from './dtos/automatic.dto'
import { GenerateDTO, DEFAULT_THEME } from './dtos/generate.dto'

dayjs.extend(utc)
Expand Down Expand Up @@ -53,32 +46,28 @@ export class CaptchaController {
return this.captchaService.generateCaptcha(theme)
}

@Post('/verify-manual')
@Post('/verify')
@HttpCode(200)
@UseGuards(BotDetectionGuard)
@BotDetection()
async verifyManual(
async verify(
@Body() manualDTO: ManualDTO,
@Req() request: Request,
@Headers() headers,
@Res({ passthrough: true }) response: Response,
@Ip() reqIP,
): Promise<any> {
this.logger.log({ manualDTO }, 'POST /captcha/verify-manual')

const { 'user-agent': userAgent } = headers
// todo: add origin checks

// @ts-ignore
const tokenCookie = request?.cookies?.[CAPTCHA_COOKIE_KEY]
const { code, hash, pid } = manualDTO

await this.captchaService.validatePIDForCAPTCHA(pid)

const timestamp = dayjs.utc().unix()

// For dummy (test) PIDs
if (pid === DUMMY_PIDS.MANUAL_PASS) {
if (pid === DUMMY_PIDS.ALWAYS_PASS) {
const dummyToken = this.captchaService.generateDummyToken()
return {
success: true,
Expand All @@ -96,32 +85,7 @@ export class CaptchaController {
throw new ForbiddenException('Incorrect captcha')
}

let decrypted

try {
decrypted = await this.captchaService.decryptTokenCaptcha(tokenCookie)
} catch (e) {
decrypted = {
manuallyVerified: 0,
automaticallyVerified: 0,
}
}

const token = await this.captchaService.generateToken(
pid,
hash,
timestamp,
false,
)

const { manuallyVerified, automaticallyVerified } =
this.captchaService.incrementManuallyVerified(decrypted)

const newTokenCookie = this.captchaService.getTokenCaptcha(
manuallyVerified,
automaticallyVerified,
)
this.captchaService.setTokenCookie(response, newTokenCookie)
const token = await this.captchaService.generateToken(pid, hash, timestamp)

const ip = getIPFromHeaders(headers) || reqIP || ''

Expand All @@ -142,116 +106,6 @@ export class CaptchaController {
}
}

@Post('/verify')
@HttpCode(200)
@UseGuards(BotDetectionGuard)
@BotDetection()
async autoVerifiable(
@Body() automaticDTO: AutomaticDTO,
@Req() request: Request,
@Headers() headers,
@Res({ passthrough: true }) response: Response,
@Ip() reqIP,
): Promise<any> {
this.logger.log(automaticDTO, 'POST /captcha/verify')

const { 'user-agent': userAgent } = headers
// todo: add origin checks

const { pid } = automaticDTO

await this.captchaService.validatePIDForCAPTCHA(pid)

// @ts-ignore
let tokenCookie = request?.cookies?.[CAPTCHA_COOKIE_KEY]

let verifiable

try {
verifiable = await this.captchaService.autoVerifiable(pid, tokenCookie)
} catch (e) {
// Either there was no cookie or the cookie was invalid
let newTokenCookie

if (
isDummyPID(pid) &&
(pid !== DUMMY_PIDS.AUTO_PASS || pid === DUMMY_PIDS.ALWAYS_FAIL)
) {
throw new ForbiddenException('Captcha required')
}

// Set a new cookie
try {
newTokenCookie = this.captchaService.getTokenCaptcha()
} catch (reason) {
console.error(reason)
throw new InternalServerErrorException(
'Could not generate a captcha cookie',
)
}

tokenCookie = newTokenCookie

this.captchaService.setTokenCookie(response, newTokenCookie)
}

if (!verifiable) {
throw new ForbiddenException('Captcha required')
}

let decrypted

const timestamp = dayjs.utc().unix()
const token = await this.captchaService.generateToken(
pid,
null,
timestamp,
true,
)

if (pid === DUMMY_PIDS.AUTO_PASS) {
return {
success: true,
token,
timestamp,
hash: null,
}
}

try {
decrypted = await this.captchaService.decryptTokenCaptcha(tokenCookie)
} catch (e) {
throw new InternalServerErrorException('Could not decrypt captcha cookie')
}

const { manuallyVerified, automaticallyVerified } =
this.captchaService.incrementAutomaticallyVerified(decrypted)

const newTokenCookie = this.captchaService.getTokenCaptcha(
manuallyVerified,
automaticallyVerified,
)

this.captchaService.setTokenCookie(response, newTokenCookie)

const ip = getIPFromHeaders(headers) || reqIP || ''

await this.captchaService.logCaptchaPass(
pid,
userAgent,
timestamp,
false,
ip,
)

return {
success: true,
token,
timestamp,
hash: null,
}
}

@Post('/validate')
@HttpCode(200)
async validateToken(@Body() validateDTO: ValidateDTO): Promise<any> {
Expand Down
Loading
Loading