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

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

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

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

TL; DR

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

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

API имеет конечную точку /auth/token, из которой веб-интерфейс может получить токен JWT, указав имя пользователя и пароль. Ответ также включает статус аутентификации, который может быть либо AUTHENTICATED , либо 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(). В соответствии с JavaDo c, это, кажется, правильное место:

Позволяет добавить дополнительный поставщик 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 сам по себе не защищает от атак воспроизведения в течение интервала времени, в течение которого код является действительным; RF C 6238 разъясняет это

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

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

Спасибо.

-

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

1 Ответ

0 голосов
/ 18 февраля 2020

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

У меня есть провайдер в виде pojo, который реализует 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);
...