Skip to content
This repository has been archived by the owner on Sep 30, 2024. It is now read-only.

feat: add brute force protection #3

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions src/main/java/dasniko/keycloak/authenticator/SmsAuthenticator.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public void authenticate(AuthenticationFlowContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
authSession.setAuthNote(SmsConstants.CODE, code);
int ttl = Integer.parseInt(config.get(SmsConstants.CODE_TTL));
authSession.setAuthNote(SmsConstants.CODE_TTL, Long.toString(System.currentTimeMillis() + (ttl * 1000L)));
authSession.setAuthNote(SmsConstants.CODE_TTL,
Long.toString(System.currentTimeMillis() + (ttl * 1000L)));
context.challenge(context.form().setAttribute("realm", context.getRealm()).createForm(TPL_CODE));
} catch (Exception e) {
log.log(java.util.logging.Level.SEVERE, "error sending code", e);
Expand Down Expand Up @@ -79,13 +80,38 @@ public void action(AuthenticationFlowContext context) {
AuthenticationSessionModel authSession = context.getAuthenticationSession();
String code = authSession.getAuthNote(SmsConstants.CODE);
String ttl = authSession.getAuthNote(SmsConstants.CODE_TTL);
String failedAttemptsString = authSession.getAuthNote(SmsConstants.FAILED_ATTEMPTS);
Integer failedAttempts = failedAttemptsString == null ? 0 : Integer.parseInt(failedAttemptsString);

if (code == null || ttl == null) {
context.failureChallenge(AuthenticationFlowError.INTERNAL_ERROR,
context.form().createErrorPage(Response.Status.INTERNAL_SERVER_ERROR));
return;
}

// check if user is locked
String lockedUntil = authSession.getAuthNote(SmsConstants.LOCKED_UNTIL);
if (lockedUntil != null && Long.parseLong(lockedUntil) > System.currentTimeMillis()) {
context.failureChallenge(AuthenticationFlowError.USER_TEMPORARILY_DISABLED,
context.form().setAttribute("realm", context.getRealm())
.setError("smsAuthCodeInvalid").createForm(TPL_CODE));
return;
} else if (lockedUntil != null) {
authSession.removeAuthNote(SmsConstants.LOCKED_UNTIL);
failedAttempts = 0;
}

Map<String, String> config = context.getRealm().getAuthenticatorConfigByAlias("SMS auth").getConfig();
if (failedAttempts >= Integer.parseInt(config.get(SmsConstants.MAX_FAILED_ATTEMPTS))) {
context.failureChallenge(AuthenticationFlowError.USER_TEMPORARILY_DISABLED,
context.form().setAttribute("realm", context.getRealm())
.setError("smsAuthCodeInvalid").createForm(TPL_CODE));
authSession.setAuthNote(SmsConstants.LOCKED_UNTIL, Long.toString(
System.currentTimeMillis() + Integer.parseInt(config.get(SmsConstants.LOCK_DURATION)) * 1000));
log.log(java.util.logging.Level.SEVERE, "user locked after too many failed attempts to enter SMS code");
return;
}

boolean isValid = enteredCode.equals(code);
if (isValid) {
if (Long.parseLong(ttl) < System.currentTimeMillis()) {
Expand All @@ -100,6 +126,7 @@ public void action(AuthenticationFlowContext context) {
// invalid
AuthenticationExecutionModel execution = context.getExecution();
if (execution.isRequired()) {
authSession.setAuthNote(SmsConstants.FAILED_ATTEMPTS, Integer.toString(failedAttempts + 1));
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS,
context.form().setAttribute("realm", context.getRealm())
.setError("smsAuthCodeInvalid").createForm(TPL_CODE));
Expand All @@ -118,7 +145,7 @@ public boolean requiresUser() {
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
String verifiedMobileNr = user.getFirstAttribute("verifiedMobileNr");
return user.getFirstAttribute(MOBILE_NUMBER_FIELD) != null && verifiedMobileNr != null
&& verifiedMobileNr.equals("true");
&& verifiedMobileNr.equals("true");
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,28 @@ public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of(
new ProviderConfigProperty(SmsConstants.CODE_LENGTH, "Code length", "The number of digits of the generated code.", ProviderConfigProperty.STRING_TYPE, 6),
new ProviderConfigProperty(SmsConstants.CODE_TTL, "Time-to-live", "The time to live in seconds for the code to be valid.", ProviderConfigProperty.STRING_TYPE, "300"),
new ProviderConfigProperty(SmsConstants.SENDER_NR, "SenderNr", "The number that is displayed as the message sender on the receiving device.", ProviderConfigProperty.STRING_TYPE, "004199999999"),
new ProviderConfigProperty(SmsConstants.USERNAME, "Username", "Ecall API username.", ProviderConfigProperty.STRING_TYPE, "user"),
new ProviderConfigProperty(SmsConstants.PASSWORD, "Password", "Ecall API password.", ProviderConfigProperty.PASSWORD, "", true),

new ProviderConfigProperty(SmsConstants.SIMULATION_MODE, "Simulation mode", "In simulation mode, the SMS won't be sent, but printed to the server logs", ProviderConfigProperty.BOOLEAN_TYPE, true)
);
new ProviderConfigProperty(SmsConstants.CODE_LENGTH, "Code length",
"The number of digits of the generated code.", ProviderConfigProperty.STRING_TYPE, 6),
new ProviderConfigProperty(SmsConstants.CODE_TTL, "Time-to-live",
"The time to live in seconds for the code to be valid.", ProviderConfigProperty.STRING_TYPE,
"300"),
new ProviderConfigProperty(SmsConstants.SENDER_NR, "SenderNr",
"The number that is displayed as the message sender on the receiving device.",
ProviderConfigProperty.STRING_TYPE, "004199999999"),
new ProviderConfigProperty(SmsConstants.USERNAME, "Username", "Ecall API username.",
ProviderConfigProperty.STRING_TYPE, "user"),
new ProviderConfigProperty(SmsConstants.PASSWORD, "Password", "Ecall API password.",
ProviderConfigProperty.PASSWORD, "", true),

new ProviderConfigProperty(SmsConstants.SIMULATION_MODE, "Simulation mode",
"In simulation mode, the SMS won't be sent, but printed to the server logs",
ProviderConfigProperty.BOOLEAN_TYPE, true),
new ProviderConfigProperty(SmsConstants.MAX_FAILED_ATTEMPTS, "Max failed attempts",
"The maximum number of failed attempts before the user is locked out.",
ProviderConfigProperty.STRING_TYPE, "10"),
new ProviderConfigProperty(SmsConstants.LOCK_DURATION, "Lock duration",
"The duration in seconds the user is locked out after the maximum number of failed attempts.",
ProviderConfigProperty.STRING_TYPE, "60"));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ public class SmsConstants {
public String SIMULATION_MODE = "simulation";
public String USERNAME = "username";
public String PASSWORD = "password";
public String FAILED_ATTEMPTS = "failedAttempts";
public String LOCKED_UNTIL = "lockedUntil";
public String MAX_FAILED_ATTEMPTS = "maxFailedAttempts";
public String LOCK_DURATION = "lockDuration";
}