Add metrics for captcha scores

This commit is contained in:
Ravi Khadiwala
2022-09-14 00:08:38 -05:00
committed by ravi-signal
parent d0a8899daf
commit c14621a09f
5 changed files with 78 additions and 69 deletions

View File

@@ -125,6 +125,7 @@ public class AccountController {
private static final String CHALLENGE_ISSUED_COUNTER_NAME = name(AccountController.class, "challengeIssued");
private static final String TWILIO_VERIFY_ERROR_COUNTER_NAME = name(AccountController.class, "twilioVerifyError");
private static final String TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME = name(AccountController.class, "twilioUndelivered");
private static final String INVALID_ACCEPT_LANGUAGE_COUNTER_NAME = name(AccountController.class, "invalidAcceptLanguage");
private static final String NONSTANDARD_USERNAME_COUNTER_NAME = name(AccountController.class, "nonStandardUsername");
@@ -134,6 +135,7 @@ public class AccountController {
private static final String COUNTRY_CODE_TAG_NAME = "countryCode";
private static final String REGION_TAG_NAME = "region";
private static final String VERIFICATION_TRANSPORT_TAG_NAME = "transport";
private static final String SCORE_TAG_NAME = "score";
private static final String VERIFY_EXPERIMENT_TAG_NAME = "twilioVerify";
@@ -231,23 +233,34 @@ public class AccountController {
String sourceHost = ForwardedIpUtil.getMostRecentProxy(forwardedFor).orElseThrow();
Optional<StoredVerificationCode> storedChallenge = pendingAccounts.getCodeForNumber(number);
CaptchaRequirement requirement = requiresCaptcha(number, transport, forwardedFor, sourceHost, captcha,
storedChallenge, pushChallenge, userAgent);
if (requirement.isCaptchaRequired()) {
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
// if there's a captcha, assess it, otherwise check if we need a captcha
final Optional<RecaptchaClient.AssessmentResult> assessmentResult = captcha
.map(captchaToken -> recaptchaClient.verify(captchaToken, sourceHost));
assessmentResult.ifPresent(result ->
Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of(
Tag.of("success", String.valueOf(result.valid())),
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, countryCode),
Tag.of(REGION_TAG_NAME, region),
Tag.of(SCORE_TAG_NAME, result.score())))
.increment());
final boolean requiresCaptcha = assessmentResult
.map(result -> !result.valid())
.orElseGet(() -> requiresCaptcha(number, transport, forwardedFor, sourceHost, storedChallenge, pushChallenge));
if (requiresCaptcha) {
captchaRequiredMeter.mark();
Metrics.counter(CHALLENGE_ISSUED_COUNTER_NAME, Tags.of(
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, Util.getCountryCode(number)),
Tag.of(REGION_TAG_NAME, Util.getRegion(number))))
.increment();
if (requirement.isAutoBlock() && shouldAutoBlock(sourceHost)) {
logger.info("Auto-block: {}", sourceHost);
abusiveHostRules.setBlockedHost(sourceHost);
}
return Response.status(402).build();
}
@@ -315,6 +328,14 @@ public class AccountController {
logger.warn("Error with Twilio Verify", throwable);
return;
}
if (enrolledInVerifyExperiment && maybeVerificationSid.isEmpty() && assessmentResult.isPresent()) {
Metrics.counter(TWILIO_VERIFY_UNDELIVERED_COUNTER_NAME, Tags.of(
Tag.of(COUNTRY_CODE_TAG_NAME, countryCode),
Tag.of(REGION_TAG_NAME, region),
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(SCORE_TAG_NAME, assessmentResult.get().score())))
.increment();
}
maybeVerificationSid.ifPresent(twilioVerificationSid -> {
StoredVerificationCode storedVerificationCodeWithVerificationSid = new StoredVerificationCode(
storedVerificationCode.getCode(),
@@ -798,37 +819,17 @@ public class AccountController {
}
}
private CaptchaRequirement requiresCaptcha(String number, String transport, String forwardedFor,
private boolean requiresCaptcha(String number, String transport, String forwardedFor,
String sourceHost,
Optional<String> captchaToken,
Optional<StoredVerificationCode> storedVerificationCode,
Optional<String> pushChallenge,
String userAgent)
{
Optional<String> pushChallenge) {
if (testDevices.containsKey(number)) {
return new CaptchaRequirement(false, false);
return false;
}
final String countryCode = Util.getCountryCode(number);
final String region = Util.getRegion(number);
if (captchaToken.isPresent()) {
boolean validToken = recaptchaClient.verify(captchaToken.get(), sourceHost);
Metrics.counter(CAPTCHA_ATTEMPT_COUNTER_NAME, Tags.of(
Tag.of("success", String.valueOf(validToken)),
UserAgentTagUtil.getPlatformTag(userAgent),
Tag.of(COUNTRY_CODE_TAG_NAME, countryCode),
Tag.of(REGION_TAG_NAME, region)))
.increment();
if (validToken) {
return new CaptchaRequirement(false, false);
} else {
return new CaptchaRequirement(true, false);
}
}
{
final List<Tag> tags = new ArrayList<>();
tags.add(Tag.of(COUNTRY_CODE_TAG_NAME, countryCode));
@@ -842,14 +843,13 @@ public class AccountController {
if (!pushChallenge.get().equals(storedPushChallenge.orElse(null))) {
tags.add(Tag.of(CHALLENGE_MATCH_TAG_NAME, "false"));
return new CaptchaRequirement(true, false);
return true;
} else {
tags.add(Tag.of(CHALLENGE_MATCH_TAG_NAME, "true"));
}
} else {
tags.add(Tag.of(CHALLENGE_PRESENT_TAG_NAME, "false"));
return new CaptchaRequirement(true, false);
return true;
}
} finally {
Metrics.counter(PUSH_CHALLENGE_COUNTER_NAME, tags).increment();
@@ -870,7 +870,7 @@ public class AccountController {
// would be caught by country filter as well
countryFilterApplicable.mark();
}
return new CaptchaRequirement(true, false);
return true;
}
try {
@@ -878,7 +878,11 @@ public class AccountController {
} catch (RateLimitExceededException e) {
logger.info("Rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
rateLimitedHostMeter.mark();
return new CaptchaRequirement(true, true);
if (shouldAutoBlock(sourceHost)) {
logger.info("Auto-block: {}", sourceHost);
abusiveHostRules.setBlockedHost(sourceHost);
}
return true;
}
try {
@@ -886,15 +890,18 @@ public class AccountController {
} catch (RateLimitExceededException e) {
logger.info("Prefix rate limit exceeded: {}, {}, {} ({})", transport, number, sourceHost, forwardedFor);
rateLimitedPrefixMeter.mark();
return new CaptchaRequirement(true, true);
if (shouldAutoBlock(sourceHost)) {
logger.info("Auto-block: {}", sourceHost);
abusiveHostRules.setBlockedHost(sourceHost);
}
return true;
}
if (countryFiltered) {
countryFilteredHostMeter.mark();
return new CaptchaRequirement(true, false);
return true;
}
return new CaptchaRequirement(false, false);
return false;
}
@Timed
@@ -941,22 +948,4 @@ public class AccountController {
return Hex.toStringCondensed(challenge);
}
private static class CaptchaRequirement {
private final boolean captchaRequired;
private final boolean autoBlock;
private CaptchaRequirement(boolean captchaRequired, boolean autoBlock) {
this.captchaRequired = captchaRequired;
this.autoBlock = autoBlock;
}
boolean isCaptchaRequired() {
return captchaRequired;
}
boolean isAutoBlock() {
return autoBlock;
}
}
}

View File

@@ -62,7 +62,7 @@ public class RateLimitChallengeManager {
rateLimiters.getRecaptchaChallengeAttemptLimiter().validate(account.getUuid());
final boolean challengeSuccess = recaptchaClient.verify(captcha, mostRecentProxyIp);
final boolean challengeSuccess = recaptchaClient.verify(captcha, mostRecentProxyIp).valid();
final Tags tags = Tags.of(
Tag.of(SOURCE_COUNTRY_TAG_NAME, Util.getCountryCode(account.getNumber())),

View File

@@ -77,7 +77,20 @@ public class RecaptchaClient {
return parts;
}
public boolean verify(final String input, final String ip) {
/**
* A captcha assessment
*
* @param valid whether the captcha was passed
* @param score string representation of the risk level
*/
public record AssessmentResult(boolean valid, String score) {
public static AssessmentResult invalid() {
return new AssessmentResult(false, "");
}
}
public AssessmentResult verify(final String input, final String ip) {
final String[] parts = parseInputToken(input);
final String sitekey = parts[0];
@@ -102,11 +115,13 @@ public class RecaptchaClient {
.increment();
if (assessment.getTokenProperties().getValid()) {
return assessment.getRiskAnalysis().getScore() >=
dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration().getScoreFloor().floatValue();
final float score = assessment.getRiskAnalysis().getScore();
return new AssessmentResult(
score >=
dynamicConfigurationManager.getConfiguration().getCaptchaConfiguration().getScoreFloor().floatValue(),
Integer.toString((int) score));
} else {
return false;
return AssessmentResult.invalid();
}
}
}