Merge pull request #3761 from omnivore-app/feat/signup-captcha

Add recaptcha on the web sign up flow
This commit is contained in:
Jackson Harper
2024-04-01 12:24:39 +08:00
committed by GitHub
7 changed files with 366 additions and 193 deletions

View File

@ -47,6 +47,7 @@ import {
} from './google_auth'
import { createWebAuthToken } from './jwt_helpers'
import { createMobileAccountCreationResponse } from './mobile/account_creation'
import { verifyChallengeRecaptcha } from '../../utils/recaptcha'
export interface SignupRequest {
email: string
@ -55,6 +56,7 @@ export interface SignupRequest {
username: string
bio?: string
pictureUrl?: string
recaptchaToken?: string
}
const signToken = promisify(jwt.sign)
@ -64,20 +66,28 @@ const cookieParams = {
maxAge: 365 * 24 * 60 * 60 * 1000,
}
const isURLPresent = (input: string): boolean => {
const urlRegex = /(https?:\/\/[^\s]+)/g
return urlRegex.test(input)
}
export const isValidSignupRequest = (obj: any): obj is SignupRequest => {
return (
'email' in obj &&
obj.email.trim().length > 0 &&
obj.email.trim().length < 512 && // email must not be empty
!isURLPresent(obj.email) &&
'password' in obj &&
obj.password.length >= 8 &&
obj.password.trim().length < 512 && // password must be at least 8 characters
'name' in obj &&
obj.name.trim().length > 0 &&
obj.name.trim().length < 512 && // name must not be empty
!isURLPresent(obj.name) &&
'username' in obj &&
obj.username.trim().length > 0 &&
obj.username.trim().length < 512 // username must not be empty
obj.username.trim().length < 512 && // username must not be empty
!isURLPresent(obj.username)
)
}
@ -499,16 +509,27 @@ export function authRouter() {
`${env.client.url}/auth/email-signup?errorCodes=INVALID_CREDENTIALS`
)
}
const { email, password, name, username, bio, pictureUrl } = req.body
const {
email,
password,
name,
username,
bio,
pictureUrl,
recaptchaToken,
} = req.body
function isURLPresent(input: string): boolean {
const urlRegex = /(https?:\/\/[^\s]+)/g
return urlRegex.test(input)
}
if (recaptchaToken && process.env.RECAPTCHA_CHALLENGE_SECRET_KEY) {
const verified = await verifyChallengeRecaptcha(recaptchaToken)
console.log('recaptcha result: ', recaptchaToken, verified)
// temporarily do not fail here so we can deploy this in stages
// just log the verification
if (isURLPresent(email) || isURLPresent(name) || isURLPresent(username)) {
res.redirect(`${env.client.url}/auth/email-signup?errorCodes=UNKNOWN`)
return
// if (!verified) {
// return res.redirect(
// `${env.client.url}/auth/email-signup?errorCodes=UNKNOWN`
// )
// }
}
// trim whitespace in email address

View File

@ -0,0 +1,39 @@
import axios from 'axios'
type RecaptchaResponse = {
success: boolean
hostname: string
score?: number
action?: string
}
export const isRecaptchaResponse = (data: any): data is RecaptchaResponse => {
return 'success' in data && 'hostname' in data
}
export const verifyChallengeRecaptcha = async (
token: string
): Promise<boolean> => {
if (!process.env.RECAPTCHA_CHALLENGE_SECRET_KEY) {
return false
}
const url = `https://www.google.com/recaptcha/api/siteverify`
const params = new URLSearchParams({
secret: process.env.RECAPTCHA_CHALLENGE_SECRET_KEY,
response: token,
})
try {
const response = await axios.post(url, params)
console.log('recaptcha response: ', response.data)
if (isRecaptchaResponse(response.data)) {
return response.data.success
}
return false
} catch (error) {
console.error('Error verifying reCAPTCHA:', error)
return false
}
}

View File

@ -18,6 +18,7 @@ import {
import * as util from '../../src/utils/sendEmail'
import { createTestUser } from '../db'
import { generateFakeUuid, request } from '../util'
import { isValidSignupRequest } from '../../src/routers/auth/auth_router'
chai.use(sinonChai)
@ -631,13 +632,60 @@ describe('auth router', () => {
'ios'
).expect(200)
const user = await userRepository.findOneByOrFail({ name })
const { count } = await searchLibraryItems(
{ query: 'in:all' },
user.id
)
const { count } = await searchLibraryItems({ query: 'in:all' }, user.id)
expect(count).to.eql(4)
})
})
})
})
describe('isValidSignupRequest', () => {
it('returns true for normal looking requests', async () => {
const result = isValidSignupRequest({
email: 'email@omnivore.app',
password: 'superDuperPassword',
name: "The User's Name",
username: 'foouser',
})
expect(result).to.be.true
})
it('returns false for requests w/missing info', async () => {
let result = isValidSignupRequest({
password: 'superDuperPassword',
name: "The User's Name",
username: 'foouser',
})
expect(result).to.be.false
result = isValidSignupRequest({
email: 'email@omnivore.app',
name: "The User's Name",
username: 'foouser',
})
expect(result).to.be.false
result = isValidSignupRequest({
email: 'email@omnivore.app',
password: 'superDuperPassword',
username: 'foouser',
})
expect(result).to.be.false
result = isValidSignupRequest({
email: 'email@omnivore.app',
password: 'superDuperPassword',
name: "The User's Name",
})
expect(result).to.be.false
})
it('returns false for requests w/malicious info', async () => {
let result = isValidSignupRequest({
password: 'superDuperPassword',
name: "You've won a cake sign up here: https://foo.bar",
username: 'foouser',
})
expect(result).to.be.false
})
})