Закрыты задачи бэклога по неочевидному поведению UI при ошибках отправки сообщений и блокировках со стороны Message-safety + добалено ограничение на размер сообщения
This commit is contained in:
@@ -100,27 +100,31 @@ final class OtpStore {
|
||||
"failure", "order_failed", device);
|
||||
}
|
||||
|
||||
boolean consume(String challengeId, String suppliedCode, DeviceMetadata device) {
|
||||
VerifyResult consume(String challengeId, String suppliedCode, DeviceMetadata device) {
|
||||
OtpChallengeEntity challenge = entityManager.find(
|
||||
OtpChallengeEntity.class, challengeId, LockModeType.PESSIMISTIC_WRITE);
|
||||
Instant now = Instant.now();
|
||||
if (challenge == null) return false;
|
||||
if (challenge == null) return VerifyResult.INVALID;
|
||||
if (!"active".equals(challenge.challengeStatus)) {
|
||||
event("otp_verify", challenge.phoneHmac, challengeId, challenge.smsMessageId,
|
||||
"already_used", challenge.challengeStatus, device);
|
||||
return false;
|
||||
return switch (challenge.challengeStatus) {
|
||||
case "limited" -> VerifyResult.VERIFY_LIMITED;
|
||||
case "expired" -> VerifyResult.EXPIRED;
|
||||
default -> VerifyResult.ALREADY_USED;
|
||||
};
|
||||
}
|
||||
if (!challenge.expiresAt.isAfter(now)) {
|
||||
challenge.challengeStatus = "expired";
|
||||
event("otp_verify", challenge.phoneHmac, challengeId, challenge.smsMessageId,
|
||||
"expired", "ttl", device);
|
||||
return false;
|
||||
return VerifyResult.EXPIRED;
|
||||
}
|
||||
if (challenge.verifyAttempts >= challenge.maxVerifyAttempts) {
|
||||
challenge.challengeStatus = "limited";
|
||||
event("otp_verify", challenge.phoneHmac, challengeId, challenge.smsMessageId,
|
||||
"limited", "attempt_limit", device);
|
||||
return false;
|
||||
return VerifyResult.VERIFY_LIMITED;
|
||||
}
|
||||
challenge.verifyAttempts++;
|
||||
boolean valid = suppliedCode != null && Crypto.constantTimeEquals(
|
||||
@@ -130,13 +134,13 @@ final class OtpStore {
|
||||
if (limited) challenge.challengeStatus = "limited";
|
||||
event("otp_verify", challenge.phoneHmac, challengeId, challenge.smsMessageId,
|
||||
limited ? "limited" : "failure", limited ? "attempt_limit" : "invalid", device);
|
||||
return false;
|
||||
return limited ? VerifyResult.VERIFY_LIMITED : VerifyResult.INVALID;
|
||||
}
|
||||
challenge.consumedAt = now;
|
||||
challenge.challengeStatus = "consumed";
|
||||
event("otp_verify", challenge.phoneHmac, challengeId, challenge.smsMessageId,
|
||||
"success", "verified", device);
|
||||
return true;
|
||||
return VerifyResult.VERIFIED;
|
||||
}
|
||||
|
||||
OtpChallengeEntity get(String challengeId) {
|
||||
@@ -193,6 +197,14 @@ final class OtpStore {
|
||||
|
||||
record Reservation(OtpChallengeEntity challenge, String otp) {}
|
||||
|
||||
enum VerifyResult {
|
||||
VERIFIED,
|
||||
INVALID,
|
||||
EXPIRED,
|
||||
ALREADY_USED,
|
||||
VERIFY_LIMITED
|
||||
}
|
||||
|
||||
static final class OtpLimitException extends RuntimeException {
|
||||
OtpLimitException(String message) { super(message); }
|
||||
|
||||
|
||||
+7
-2
@@ -66,8 +66,13 @@ public final class PhoneOtpAuthenticator implements Authenticator {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!new OtpStore(context.getSession()).consume(challengeId, code, device)) {
|
||||
Response response = otpForm(context, PhoneNormalizer.mask(phone), "otpInvalid");
|
||||
OtpStore.VerifyResult result =
|
||||
new OtpStore(context.getSession()).consume(challengeId, code, device);
|
||||
if (result != OtpStore.VerifyResult.VERIFIED) {
|
||||
String messageKey = result == OtpStore.VerifyResult.VERIFY_LIMITED
|
||||
? "otpVerifyLimited"
|
||||
: "otpInvalid";
|
||||
Response response = otpForm(context, PhoneNormalizer.mask(phone), messageKey);
|
||||
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, response);
|
||||
return;
|
||||
}
|
||||
|
||||
+16
@@ -28,6 +28,10 @@ class SmsLifecycleContractTest {
|
||||
void otpThemeUsesSnapshotLengthExpiryAndRealResendAction() throws Exception {
|
||||
String template = Files.readString(Path.of("themes/han-phone/login/otp.ftl"));
|
||||
String script = Files.readString(Path.of("themes/han-phone/login/resources/js/han-login.js"));
|
||||
String messages = Files.readString(
|
||||
Path.of("themes/han-phone/login/messages/messages_ru.properties"));
|
||||
String verifier = Files.readString(Path.of(
|
||||
"src/main/java/ru/han/chat/keycloak/PhoneOtpAuthenticator.java"));
|
||||
|
||||
assertTrue(template.contains("otpCodeLength"));
|
||||
assertTrue(template.contains("otpExpiresAt"));
|
||||
@@ -37,6 +41,18 @@ class SmsLifecycleContractTest {
|
||||
assertTrue(script.contains("han_device_id"));
|
||||
assertTrue(script.contains("expiresAt - Date.now()"));
|
||||
assertTrue(script.contains("Number.isFinite(expiresAt)"));
|
||||
assertTrue(messages.contains("otpVerifyLimited="));
|
||||
assertTrue(messages.contains("Попробуйте через 24 часа"));
|
||||
assertTrue(verifier.contains("VerifyResult.VERIFY_LIMITED"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void phoneThemeDoesNotRepeatConsentText() throws Exception {
|
||||
String phone = Files.readString(Path.of("themes/han-phone/login/phone.ftl"));
|
||||
|
||||
assertTrue(!phone.contains("phoneLegalPrefix"));
|
||||
assertTrue(!phone.contains("termsOfUse"));
|
||||
assertTrue(!phone.contains("privacyPolicy"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -20,8 +20,9 @@ verifyOtp=Подтвердить
|
||||
mockMode=Тестовый режим отправки кода
|
||||
phoneInvalid=Проверьте формат номера телефона.
|
||||
otpInvalid=Код неверен, истёк или уже использован.
|
||||
otpVerifyLimited=Превышен лимит неуспешных авторизаций. Начните процедуру заново.
|
||||
otpCooldown=Повторно отправить СМС можно после обнуления таймера.
|
||||
otpLimited=Слишком много попыток. Повторите позже.
|
||||
otpLimited=Превышен лимит попыток авторизации. Попробуйте через 24 часа.
|
||||
otpUnavailable=Сервис подтверждения временно недоступен. Повторите позже.
|
||||
captchaInvalid=Не удалось подтвердить, что запрос отправил человек. Пройдите проверку ещё раз.
|
||||
captchaUnavailable=Проверка пока не загрузилась. Проверьте соединение и повторите.
|
||||
|
||||
@@ -57,12 +57,6 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<p class="han-legal">
|
||||
${msg("phoneLegalPrefix")}
|
||||
<span>${msg("termsOfUse")}</span>
|
||||
${msg("phoneLegalAnd")}
|
||||
<span>${msg("privacyPolicy")}</span>
|
||||
</p>
|
||||
</div>
|
||||
<script src="${url.resourcesPath}/js/han-login.js?v=5"></script>
|
||||
<#if captchaEnabled!false>
|
||||
|
||||
@@ -246,20 +246,6 @@ body.login-pf {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.han-legal {
|
||||
margin: 32px 0 0;
|
||||
color: var(--han-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.han-legal span {
|
||||
color: rgba(37, 37, 37, 0.7);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.han-error {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
Reference in New Issue
Block a user