Add metrics for captcha scores
This commit is contained in:
parent
d0a8899daf
commit
c14621a09f
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,7 +68,10 @@ class RateLimitChallengeManagerTest {
|
|||
when(account.getNumber()).thenReturn("+18005551234");
|
||||
when(account.getUuid()).thenReturn(UUID.randomUUID());
|
||||
|
||||
when(recaptchaClient.verify(any(), any())).thenReturn(successfulChallenge);
|
||||
when(recaptchaClient.verify(any(), any()))
|
||||
.thenReturn(successfulChallenge
|
||||
? new RecaptchaClient.AssessmentResult(true, "")
|
||||
: RecaptchaClient.AssessmentResult.invalid());
|
||||
|
||||
when(rateLimiters.getRecaptchaChallengeAttemptLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
when(rateLimiters.getRecaptchaChallengeSuccessLimiter()).thenReturn(mock(RateLimiter.class));
|
||||
|
|
|
@ -300,8 +300,10 @@ class AccountControllerTest {
|
|||
when(abusiveHostRules.isBlocked(eq(ABUSIVE_HOST))).thenReturn(true);
|
||||
when(abusiveHostRules.isBlocked(eq(NICE_HOST))).thenReturn(false);
|
||||
|
||||
when(recaptchaClient.verify(eq(INVALID_CAPTCHA_TOKEN), anyString())).thenReturn(false);
|
||||
when(recaptchaClient.verify(eq(VALID_CAPTCHA_TOKEN), anyString())).thenReturn(true);
|
||||
when(recaptchaClient.verify(eq(INVALID_CAPTCHA_TOKEN), anyString()))
|
||||
.thenReturn(RecaptchaClient.AssessmentResult.invalid());
|
||||
when(recaptchaClient.verify(eq(VALID_CAPTCHA_TOKEN), anyString()))
|
||||
.thenReturn(new RecaptchaClient.AssessmentResult(true, ""));
|
||||
|
||||
doThrow(new RateLimitExceededException(Duration.ZERO)).when(pinLimiter).validate(eq(SENDER_OVER_PIN));
|
||||
|
||||
|
|
Loading…
Reference in New Issue