Merge pull request #3761 from omnivore-app/feat/signup-captcha
Add recaptcha on the web sign up flow
This commit is contained in:
@ -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
|
||||
|
||||
39
packages/api/src/utils/recaptcha.ts
Normal file
39
packages/api/src/utils/recaptcha.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user