Многофакторная аутентификация с использованием Spring Boot 2 и Spring Security 5

11

Я хочу добавить многофакторную аутентификацию с помощью программных токенов TOTP в приложение Angular & Spring, сохранив все как можно ближе к значениям по умолчанию Spring Boot Security Starter .

Проверка токена происходит локально (с помощью библиотеки aerogear-otp-java), стороннего поставщика API нет.

Настройка токенов для пользователя работает, но проверка их с помощью диспетчера / провайдеров аутентификации Spring Security - нет.

TL; DR

  • Каков официальный способ интеграции дополнительного AuthenticationProvider в систему, настроенную для Spring Boot Security Starter ?
  • Каковы рекомендуемые способы предотвращения повторных атак?

Длинная версия

У API есть конечная точка, /auth/tokenиз которой веб-интерфейс может получить токен JWT, указав имя пользователя и пароль. Ответ также включает в себя аутентификацию-статус, который может быть либо аутентифицирован или PRE_AUTHENTICATED_MFA_REQUIRED .

Если пользователю требуется MFA, токен выдается с единственными предоставленными полномочиями PRE_AUTHENTICATED_MFA_REQUIREDи сроком действия 5 минут. Это позволяет пользователю получить доступ к конечной точке, /auth/mfa-tokenгде он может предоставить код TOTP из своего приложения Authenticator и получить полностью аутентифицированный токен для доступа к сайту.

Провайдер и токен

Я создал свой кастом, MfaAuthenticationProviderкоторый реализует AuthenticationProvider:

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        // validate the OTP code
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return OneTimePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

И, OneTimePasswordAuthenticationTokenкоторый расширяется AbstractAuthenticationTokenдля хранения имени пользователя (взятого из подписанного JWT) и кода OTP.

конфиг

У меня есть свой обычай WebSecurityConfigurerAdapter, куда я добавляю свой обычай AuthenticationProviderчерез http.authenticationProvider(). В соответствии с JavaDoc, это, кажется, правильное место:

Позволяет добавить дополнительный AuthenticationProvider для использования

Соответствующие части моей SecurityConfigвыглядит следующим образом.

    @Configuration
    @EnableWebSecurity
    @EnableJpaAuditing(auditorAwareRef = "appSecurityAuditorAware")
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        private final TokenProvider tokenProvider;

        public SecurityConfig(TokenProvider tokenProvider) {
            this.tokenProvider = tokenProvider;
        }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authenticationProvider(new MfaAuthenticationProvider());

        http.authorizeRequests()
            // Public endpoints, HTML, Assets, Error Pages and Login
            .antMatchers("/", "favicon.ico", "/asset/**", "/pages/**", "/api/auth/token").permitAll()

            // MFA auth endpoint
            .antMatchers("/api/auth/mfa-token").hasAuthority(ROLE_PRE_AUTH_MFA_REQUIRED)

            // much more config

контроллер

AuthControllerИмеет AuthenticationManagerBuilderвпрыскивается и тянет все это вместе.

@RestController
@RequestMapping(AUTH)
public class AuthController {
    private final TokenProvider tokenProvider;
    private final AuthenticationManagerBuilder authenticationManagerBuilder;

    public AuthController(TokenProvider tokenProvider, AuthenticationManagerBuilder authenticationManagerBuilder) {
        this.tokenProvider = tokenProvider;
        this.authenticationManagerBuilder = authenticationManagerBuilder;
    }

    @PostMapping("/mfa-token")
    public ResponseEntity<Token> mfaToken(@Valid @RequestBody OneTimePassword oneTimePassword) {
        var username = SecurityUtils.getCurrentUserLogin().orElse("");
        var authenticationToken = new OneTimePasswordAuthenticationToken(username, oneTimePassword.getCode());
        var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // rest of class

Однако публикация против /auth/mfa-tokenприводит к этой ошибке:

"error": "Forbidden",
"message": "Access Denied",
"trace": "org.springframework.security.authentication.ProviderNotFoundException: No AuthenticationProvider found for de.....OneTimePasswordAuthenticationToken

Почему Spring Security не получает мой провайдер аутентификации? Отладка контроллера показывает мне, что DaoAuthenticationProviderэто единственный поставщик аутентификации в AuthenticationProviderManager.

Если я выставляю свой MfaAuthenticationProviderбин, это единственный поставщик, который зарегистрирован, поэтому я получаю обратное:

No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken. 

Итак, как мне получить оба?

Мой вопрос

Каков рекомендуемый способ интеграции дополнительного устройства AuthenticationProviderв систему, настроенную для Spring Boot Security Starter , чтобы я мог получить и свой, DaoAuthenticationProviderи свой собственный пользовательский MfaAuthenticationProvider? Я хочу сохранить настройки Spring Boot Scurity Starter по умолчанию и дополнительно иметь своего провайдера.

Предотвращение повторной атаки

Я знаю, что алгоритм OTP сам по себе не защищает от атак воспроизведения в течение интервала времени, в течение которого код является действительным; RFC 6238 проясняет это

Верификатор НЕ ДОЛЖЕН принимать вторую попытку OTP после успешной проверки для первого OTP, что гарантирует одноразовое использование OTP.

Мне было интересно, если есть рекомендуемый способ реализации защиты. Поскольку токены OTP основаны на времени, я думаю о сохранении последнего успешного входа в модель пользователя и о том, что на интервал времени 30 секунд должен быть только один успешный вход. Это, конечно, означает синхронизацию на пользовательской модели. Любые лучшие подходы?

Спасибо.

-

PS: так как это вопрос безопасности, я ищу ответ из достоверных и / или официальных источников. Спасибо.

phisch
источник

Ответы:

0

Чтобы ответить на мой собственный вопрос, вот как я это реализовал после дальнейших исследований.

У меня есть провайдер как ПОЖО, который реализует AuthenticationProvider. Это намеренно не Бин / Компонент. В противном случае Spring зарегистрирует его как единственного провайдера.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

    @Override
    public Authentication authenticate(Authentication authentication) {
        // here be code 
        }

В моем SecurityConfig я позволил Spring автоматически подключить AuthenticationManagerBuilderи вручную ввести мойMfaAuthenticationProvider

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
       private final AuthenticationManagerBuilder authenticationManagerBuilder;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // other code  
        authenticationManagerBuilder.authenticationProvider(getMfaAuthenticationProvider());
        // more code
}

// package private for testing purposes. 
MfaAuthenticationProvider getMfaAuthenticationProvider() {
    return new MfaAuthenticationProvider(accountService);
}

После стандартной аутентификации, если у пользователя включен MFA, он предварительно аутентифицируется с предоставленными полномочиями PRE_AUTHENTICATED_MFA_REQUIRED . Это позволяет им получить доступ к одной конечной точке /auth/mfa-token. Эта конечная точка берет имя пользователя из действительного JWT и предоставленного TOTP и отправляет его в authenticate()метод authenticationManagerBuilder, который выбирает способ MfaAuthenticationProviderобработки OneTimePasswordAuthenticationToken.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
phisch
источник