null

Двухфакторная аутентификация через SMS в Keycloak.

Защита личных аккаунтов только при помощи пароля - не самый надежный способ обезопасить пользователей от взлома. Часто в роли паролей используются закономерные последовательности, или же пароль может быть скомпрометирован: обманные сообщения или похожие сайты часто заставляют людей делиться паролями. Мошенничества с кражей паролей частое явление в сети.

Двухфакторная аутентификация увеличивает степень безопасности аккаунтов: даже если злоумышленнику удастся скомпрометировать пароль, ему нужно получить мобильный телефон жертвы для дальнейших действий. Впрочем, двухфакторная защита не панацея от взлома аккаунта, но достаточно надежный барьер, который серьезно усложняет злоумышленникам доступ к чужим данным. К тому же двухфакторная аутентификация в какой-то степени нивелирует недостатки классической парольной защиты.
 

Что такое двухфакторная авторизация - Digital Dealerz

Начало работы

Реализовать процесс 2FA самостоятельно не сложно, Keycloak содержит необходимые инструменты для этого. 

Дисклеймер: представленный провайдер двухфакторного аутентификатора предназначен только для демонстрационных целей. Если вы 
хотите использовать его в производственных целях, скорее всего, придется расширить код дополнительными проверками и валидацией!

Структура двухфакторной аутентификации в Keycloak делится на 2 логические части: сервис отправки сообщений и аутентификатора. К части сервиса отправки сообщений относится логика отправки сообщений :). В примере показана работа с SMS-подтверждением, но двухфакторную защиту также можно реализовать при помощи email-подтверждений или других каналов связи. В аутентификаторе находится бизнес-логика логина: создание и сохранение кода, проверка срока действия и проверка на валидность после получения. 
 

Пример структуры проекта:
 

--------project
----------------authenticator
------------------------Keycloak2FAAuthenticator.java
------------------------Keycloak2FAAuthenticatorFactory.java
----------------services
------------------------sms
--------------------------------KeycloakSmsService.java
--------------------------------KeycloakSmsServiceFactory.java
--------------------------------KeycloakSmsServiceImpl.java
------------------------email
--------------------------------KeycloakEmailService.java
--------------------------------KeycloakEmailServiceFactory.java
--------------------------------KeycloakEmailServiceImpl.java


Сервис отправки сообщений

Начнем с самого простого: определим интерфейс сервиса отправки сообщений KeycloakSmsService. Этот сервис должен иметь метод для отправки сообщения по номеру телефона.
 

public interface KeycloakSmsService {

    void send(String phoneNumber, String message);

}

Реализация метода send  находится в KeycloakSmsServiceImpl, реализующий KeycloakSmsService. Клиента для отправки сообщений стоит выбирать на основании предпочтений и требований, в статье на этом подробно останавливаться не будем. В примере используется MessageBird.

@Override
public void send(String phoneNumber, String message) {
        messageBirdClient.sendMessage(new Message(originator,message, Arrays.asList(parseNumber(phoneNumber))));
}

// обработка полученного номера телефона
private BigInteger parseNumber(String number){
    number = number.replaceAll("[^0-9]+","");
    return BigInteger.valueOf(Long.parseLong(number));
}

 

Фабрика отправки сообщений

Чтобы не тратить деньги за отправку сообщения впустую при тестировании двухфакторной аутентификации, в разработанной админке настраивается флаг TEST_MODE - указание на тестовый режим, в котором сообщения выводятся в лог без отправки на мобильный телефон. Добавление настройки тестового режима в админку происходит в Keycloak2FAAuthenticatorFactory.

В фабрике KeycloakSmsServiceFactory устанавливается метод для обработки отправки сообщения, в зависимости от того состояния флага тестового режима.

public class KeycloakSmsServiceFactory {

    public static KeycloakSmsService get(Map<String, String> config) {
        if (Boolean.parseBoolean(config.getOrDefault("test", "false"))) {
	// вывод сообщения в консоль
            return (phoneNumber, message) ->
                    log.info(String.format("***** TEST MODE *****\n Would send SMS to %s with text: %s", phoneNumber, message));
        } else {
            return new KeycloakSmsServiceImpl(config.get("accessKey"), config.get("originator"));
        }
    }
}

 

Аутентификатор
 

Класс аутентификатора реализует класс Authenticator из пакета org.keycloak.authentication.Authenticator.

public class Keycloak2FAAuthenticator implements Authenticator

 

Метод authenticate выполняется ДО запуска сеанса аутентификации этого аутентификатора. Здесь проверяется включил ли пользователь двухфакторную аутентификацию для аккаунта, генерируется одноразовый код, сохраняются параметры и отправляется сообщение.

В реализации у пользователя в атрибутах хранится номер мобильного телефона MOBILE_NUMBER и признак включения двухфакторной авторизации у этого пользователя TWO_FA_ENABLED. Конфигурации для конкретного случая двухфакторной авторизации в примере настраиваются через админку Keycloak, а общие параметры для конфигураций создаются в Keycloak2FAAuthenticatorFactory.java. После отправки сообщения с кодом в интерфейс встраивается форма из файла LOGIN_2FA_FORM, в authSession сохраняются текущие параметры сессии пользователя: количество использованных попыток, корректный код и дата истечения срока жизни кода.

@Override
public void authenticate(AuthenticationFlowContext context) {

    // получение пользователя из контекста
    KeycloakSession session = context.getSession();
    AuthenticatorConfigModel config = context.getAuthenticatorConfig();
    UserModel user = context.getUser();

    // проверка, включена ли у пользователя двухфакторная авторизация
    if (user.getFirstAttribute(AttributesName.TWO_FA_ENABLED.getName()) == null || !Boolean.parseBoolean(user.getFirstAttribute(AttributesName.TWO_FA_ENABLED.getName()))) {
        // если двухфакторная авторизация не включена - сразу логинить пользователя
        context.success();
        return;
    }
	
    // получение конфигураций из Keycloak
    int length = Integer.parseInt(config.getConfig().get("length"));
    int ttl = Integer.parseInt(config.getConfig().get("ttl"));
    // генерация кода и отправляемого сообщения желаемым способом
    String code = generateCode(length);
    String smsText = generateSmsText(lenght); 
    // получение номера мобильного телефона из атрибутов пользователя
    String mobileNumber = user.getFirstAttribute(AttributesName.MOBILE_NUMBER.getName());

    // получение сессии пользователя
    AuthenticationSessionModel authSession = context.getAuthenticationSession();
    // отправка сообщения
    KeycloakSmsServiceFactory.get(config.getConfig()).send(mobileNumber, smsText);
    // установка формы ввода кода
    context.challenge(context.form().setAttribute("realm", context.getRealm()).setAttribute("type", "sms").createForm(LOGIN_2FA_FORM));
	
    // установка атрибутов сессии для дальнейшей проверки
    authSession.setAuthNote("code", code);
    authSession.setAuthNote("attempt", "0");
    authSession.setAuthNote("ttl", Long.toString(System.currentTimeMillis() + (ttl * 1000)));
}

Метод action выполнится после получения SMS и отправки формы. Здесь выполняется дальнейшая обработка на основе информации, сохраненной в authSession: проверяется количество использованных попыток, истечение срока действия кода и сравнивается введенный код с корректным.
 

@Override
public void action(AuthenticationFlowContext context) {
    // получение введенного кода
    String enteredCode = context.getHttpRequest().getDecodedFormParameters().getFirst("code");

    // получение сессии и связанных с ней параметров
    AuthenticationSessionModel authSession = context.getAuthenticationSession();
    String code = authSession.getAuthNote("code");
    String ttl = authSession.getAuthNote("ttl");
    Long attempt = Long.parseLong(authSession.getAuthNote("attempt"));

    // проверка валидности кода
    boolean isValid = enteredCode.equals(code);

    if (Long.parseLong(ttl) < System.currentTimeMillis()) {
        // обработка истекшего кода            
        context.failureChallenge(AuthenticationFlowError.EXPIRED_CODE, context.form().setError("2FAAuthCodeExpired").createForm(TWO_FA_VALIDATION_ERROR_FTL));
        return;
    }

    if (isValid) {
        // обработка валидного кода
        context.success();
    } else {
        // обработка невалидного кода
        AuthenticationExecutionModel execution = context.getExecution();
	// увеличение счетчика попыток
        attempt++;
        authSession.setAuthNote("attempt", attempt.toString());
              
	// обработка истекших попыток
        if (attempt >= 3) {
            authSession.setAuthNote("attempt", "0");
            context.failureChallenge(AuthenticationFlowError.EXPIRED_CODE,
            context.form().setError("2FAAuthAttemptExpired").createForm(TWO_FA_VALIDATION_ERROR_FTL));
        } else if (execution.isRequired()) {
            context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS,
            context.form().setAttribute("realm", context.getRealm())
                   .setError("2FAAuthCodeInvalid").createForm(LOGIN_2FA_FTL));
        } else if (execution.isConditional() || execution.isAlternative()) {
            context.attempted();
        }
    }
}

 

Фабрика аутентификатора

Фабрика аутентификатора представлена в классе Keycloak2FAAuthenticatorFactory. Фабрика создает параметры для настройки двухфакторной аутентификации, которые устанавливаются администратором при настройке двухфакторной аутентификации. Здесь появляются настройки времени жизни кода в секундах и количества символов и включение тестового режима.

public class Keycloak2FAAuthenticatorFactory implements AuthenticatorFactory, ServerInfoAwareProviderFactory

 

При переопределении класса getRequirementChoices настраивается возможные варианты выбора requirenment’ов. В примере: REQUIRED, ALTERNATIVE и DISABLED.
 

    @Override
    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
        return new AuthenticationExecutionModel.Requirement[]{
                AuthenticationExecutionModel.Requirement.REQUIRED,
                AuthenticationExecutionModel.Requirement.ALTERNATIVE,
                AuthenticationExecutionModel.Requirement.DISABLED,
        };
    }

 

В классе getConfigProperties создаются настройки параметры конфигурации двухфакторной авторизации. Для каждого параметра необходимо передать: имя параметра, отображаемое название, описание, тип и дефолтное значение.

@Override
    public List<ProviderConfigProperty> getConfigProperties() {
        return Arrays.asList(
                new ProviderConfigProperty("length", "Code length", "Generated code length. Values from 3 to 10", ProviderConfigProperty.STRING_TYPE, 4),
                new ProviderConfigProperty("ttl", "Time-to-live", "Lifetime in seconds when the code is valid.", ProviderConfigProperty.STRING_TYPE, "300"),
                new ProviderConfigProperty("originator", "Originator", "Originator for messagebird.com", ProviderConfigProperty.STRING_TYPE, "Org"),
                new ProviderConfigProperty("accessKey", "AccessKey", "Access key for messagebird.com", ProviderConfigProperty.STRING_TYPE, ""),
                new ProviderConfigProperty("test", "Test mode", "Work without sending a code, code is displayed in the logs.", ProviderConfigProperty.BOOLEAN_TYPE, true)
        );
    }

Метод create создает новый экземпляр класса.

   @Override
    public Authenticator create(KeycloakSession session) {
        return new Keycloak2FAAuthenticator();
    }

 

Остальные методы, которые необходимо переопределить:
 

 @Override
    public String getId() {
        return "2fa-authenticator";
    }

    @Override
    public String getDisplayType() {
        return "2FA Authentication";
    }

    @Override
    public String getHelpText() {
        return "2fa with SMS or EMAIL.";
    }

    @Override
    public String getReferenceCategory() {
        return "otp";
    }

    @Override
    public boolean isConfigurable() {
        return true;
    }

    @Override
    public boolean isUserSetupAllowed() {
        return false;
    }

   @Override
    public Authenticator create(KeycloakSession session) {
        return new Keycloak2FAAuthenticator();
    }

    @Override
    public void init(Config.Scope config) {
    }

    @Override
    public void postInit(KeycloakSessionFactory factory) {
    }

    @Override
    public void close() {
    }

    @Override
    public Map<String, String> getOperationalInfo() {
        return Collections.singletonMap("Version", "demo");
    }

Настройка в админке

 

Когда кодовая база готова, остается настроить двухфакторную аутентификацию в админке Keycloak:
 

1.  Перейти во вкладку Authentication, создать новый flow с настройкам

2. Ввести необходимые параметры

 

Для включения двухфакторной аутентификации у пользователя необходимо добавить 
и определить атрибуты: TWO_FA_ENABLED и MOBILE_NUMBER, которые должны определяться при включении двухфакторной авторизации пользователем в бизнес-логике приложения.

Получайте удовольствие при двухфакторной аутентификации на основе SMS в Keycloak :)