mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-27 02:51:57 +02:00
* [web] double down on validating email addresses * [web] normalize emails in captcha middleware * [web] add support for regex based allow-list for skipping captcha * [web] skip captcha for trusted users on all actions GitOrigin-RevId: a994ebf6b74e80f462d2dab1fe5113bbffa676a9
120 lines
3.9 KiB
JavaScript
120 lines
3.9 KiB
JavaScript
const { fetchJson } = require('@overleaf/fetch-utils')
|
|
const logger = require('@overleaf/logger')
|
|
const Settings = require('@overleaf/settings')
|
|
const Metrics = require('@overleaf/metrics')
|
|
const OError = require('@overleaf/o-error')
|
|
const DeviceHistory = require('./DeviceHistory')
|
|
const AuthenticationController = require('../Authentication/AuthenticationController')
|
|
const { expressify } = require('@overleaf/promise-utils')
|
|
const EmailsHelper = require('../Helpers/EmailHelper')
|
|
|
|
function respondInvalidCaptcha(req, res) {
|
|
res.status(400).json({
|
|
errorReason: 'cannot_verify_user_not_robot',
|
|
message: {
|
|
text: req.i18n.translate('cannot_verify_user_not_robot'),
|
|
},
|
|
})
|
|
}
|
|
|
|
async function initializeDeviceHistory(req) {
|
|
req.deviceHistory = new DeviceHistory()
|
|
try {
|
|
await req.deviceHistory.parse(req)
|
|
} catch (err) {
|
|
logger.err({ err }, 'cannot parse deviceHistory')
|
|
}
|
|
}
|
|
|
|
async function canSkipCaptcha(req, res) {
|
|
const trustedUser =
|
|
req.body?.email && Settings.recaptcha.trustedUsers.includes(req.body.email)
|
|
if (trustedUser) {
|
|
return res.json(true)
|
|
}
|
|
await initializeDeviceHistory(req)
|
|
const canSkip = req.deviceHistory.has(req.body?.email)
|
|
Metrics.inc('captcha_pre_flight', 1, {
|
|
status: canSkip ? 'skipped' : 'missing',
|
|
})
|
|
res.json(canSkip)
|
|
}
|
|
|
|
function validateCaptcha(action) {
|
|
return expressify(async function (req, res, next) {
|
|
const email = EmailsHelper.parseEmail(req.body?.email)
|
|
const trustedUser =
|
|
email &&
|
|
(Settings.recaptcha.trustedUsers.includes(email) ||
|
|
Settings.recaptcha.trustedUsersRegex?.test(email))
|
|
if (!Settings.recaptcha?.siteKey || Settings.recaptcha.disabled[action]) {
|
|
if (action === 'login') {
|
|
AuthenticationController.setAuditInfo(req, { captcha: 'disabled' })
|
|
}
|
|
Metrics.inc('captcha', 1, { path: action, status: 'disabled' })
|
|
return next()
|
|
}
|
|
if (trustedUser) {
|
|
if (action === 'login') {
|
|
AuthenticationController.setAuditInfo(req, { captcha: 'trusted' })
|
|
}
|
|
Metrics.inc('captcha', 1, { path: action, status: 'trusted' })
|
|
return next()
|
|
}
|
|
const reCaptchaResponse = req.body['g-recaptcha-response']
|
|
if (action === 'login') {
|
|
await initializeDeviceHistory(req)
|
|
const fromKnownDevice = req.deviceHistory.has(email)
|
|
AuthenticationController.setAuditInfo(req, { fromKnownDevice })
|
|
if (!reCaptchaResponse && fromKnownDevice) {
|
|
// The user has previously logged in from this device, which required
|
|
// solving a captcha or keeping the device history alive.
|
|
// We can skip checking the (missing) captcha response.
|
|
AuthenticationController.setAuditInfo(req, { captcha: 'skipped' })
|
|
Metrics.inc('captcha', 1, { path: action, status: 'skipped' })
|
|
return next()
|
|
}
|
|
}
|
|
if (!reCaptchaResponse) {
|
|
Metrics.inc('captcha', 1, { path: action, status: 'missing' })
|
|
return respondInvalidCaptcha(req, res)
|
|
}
|
|
|
|
let body
|
|
try {
|
|
body = await fetchJson(Settings.recaptcha.endpoint, {
|
|
method: 'POST',
|
|
body: new URLSearchParams([
|
|
['secret', Settings.recaptcha.secretKey],
|
|
['response', reCaptchaResponse],
|
|
]),
|
|
})
|
|
} catch (err) {
|
|
Metrics.inc('captcha', 1, { path: action, status: 'error' })
|
|
throw OError.tag(err, 'failed recaptcha siteverify request', {
|
|
body: err.body,
|
|
})
|
|
}
|
|
|
|
if (!body.success) {
|
|
logger.warn(
|
|
{ statusCode: 200, body },
|
|
'failed recaptcha siteverify request'
|
|
)
|
|
Metrics.inc('captcha', 1, { path: action, status: 'failed' })
|
|
return respondInvalidCaptcha(req, res)
|
|
}
|
|
Metrics.inc('captcha', 1, { path: action, status: 'solved' })
|
|
if (action === 'login') {
|
|
AuthenticationController.setAuditInfo(req, { captcha: 'solved' })
|
|
}
|
|
next()
|
|
})
|
|
}
|
|
|
|
module.exports = {
|
|
respondInvalidCaptcha,
|
|
validateCaptcha,
|
|
canSkipCaptcha: expressify(canSkipCaptcha),
|
|
}
|