Uwierzytelnianie wieloskładnikowe za pomocą Spring Boot 2 i Spring Security 5

11

Chcę dodać uwierzytelnianie wieloskładnikowe za pomocą miękkich tokenów TOTP do aplikacji Angular & Spring, jednocześnie utrzymując wszystko tak blisko, jak to możliwe, do domyślnych ustawień Spring Boot Security Starter .

Sprawdzanie poprawności tokenów odbywa się lokalnie (z biblioteką aerogear-otp-java), bez zewnętrznego dostawcy API.

Konfigurowanie tokenów dla użytkownika działa, ale sprawdzanie ich poprawności za pomocą Menedżera / dostawców uwierzytelniania Spring Security nie działa.

TL; DR

  • Jaki jest oficjalny sposób zintegrowania dodatkowego dostawcy uwierzytelniania z systemem skonfigurowanym w Spring Boot Security Starter ?
  • Jakie są zalecane sposoby zapobiegania atakom powtórkowym?

Długa wersja

Interfejs API ma punkt końcowy, /auth/tokenz którego interfejs użytkownika może uzyskać token JWT, podając nazwę użytkownika i hasło. Odpowiedź zawiera również status uwierzytelnienia, który może mieć wartość AUTHENTICATED lub PRE_AUTHENTICATED_MFA_REQUIRED .

Jeśli użytkownik wymaga MFA, token jest wydawany z pojedynczym przyznanym uprawnieniem PRE_AUTHENTICATED_MFA_REQUIREDi czasem wygaśnięcia wynoszącym 5 minut. Dzięki temu użytkownik może uzyskać dostęp do punktu końcowego, w /auth/mfa-tokenktórym może podać kod TOTP z aplikacji Authenticator i uzyskać w pełni uwierzytelniony token w celu uzyskania dostępu do witryny.

Dostawca i token

Stworzyłem swój zwyczaj, MfaAuthenticationProviderktóry implementuje AuthenticationProvider:

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

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

I OneTimePasswordAuthenticationTokenktóra rozciąga AbstractAuthenticationTokensię na nazwę użytkownika (wziętą z podpisanego JWT) i kodu OTP.

Config

Mam swój zwyczaj WebSecurityConfigurerAdapter, do którego dodaję swój zwyczaj AuthenticationProviderprzez http.authenticationProvider(). Zgodnie z JavaDoc wydaje się to właściwe miejsce:

Umożliwia dodanie dodatkowego dostawcy uwierzytelnienia, który będzie używany

Odpowiednie części mojego SecurityConfigwygląda tak.

    @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

Kontroler

AuthControllerMa AuthenticationManagerBuilderwstrzykiwany i ciągnie to wszystko razem.

@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

Jednak wysyłanie wiadomości przeciw /auth/mfa-tokenprowadzi do tego błędu:

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

Dlaczego Spring Security nie odbiera mojego dostawcy uwierzytelniania? Debugowanie kontrolera pokazuje, że DaoAuthenticationProviderjest to jedyny dostawca uwierzytelniania w AuthenticationProviderManager.

Jeśli ujawnię moją MfaAuthenticationProviderfasolę, jest to jedyny zarejestrowany dostawca, więc otrzymam odwrotność:

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

Jak mogę uzyskać oba?

Moje pytanie

Jaki jest zalecany sposób zintegrowania dodatkowej części AuthenticationProviderze skonfigurowanym systemem Spring Boot Security Starter , aby uzyskać zarówno DaoAuthenticationProviderniestandardowe , jak i własne MfaAuthenticationProvider? Chcę zachować domyślne ustawienia Spring Boot Scurity Starter i dodatkowo mieć własnego dostawcę.

Zapobieganie atakowi powtórkowemu

Wiem, że algorytm OTP sam w sobie nie chroni przed atakami powtórkowymi w przedziale czasowym, w którym kod jest prawidłowy; RFC 6238 wyjaśnia to

Weryfikator NIE MOŻE zaakceptować drugiej próby OTP po wydaniu udanej walidacji dla pierwszego OTP, co zapewnia jednorazowe użycie OTP.

Zastanawiałem się, czy istnieje zalecany sposób wdrożenia ochrony. Ponieważ tokeny OTP są oparte na czasie, myślę o przechowywaniu ostatniego udanego logowania w modelu użytkownika i upewnieniu się, że jest tylko jedno udane logowanie na 30-sekundowy przedział czasu. To oczywiście oznacza synchronizację w modelu użytkownika. Jakieś lepsze podejścia?

Dziękuję Ci.

-

PS: ponieważ jest to pytanie dotyczące bezpieczeństwa, szukam odpowiedzi pochodzącej z wiarygodnych i / lub oficjalnych źródeł. Dziękuję Ci.

phisch
źródło

Odpowiedzi:

0

Aby odpowiedzieć na moje pytanie, tak to wdrożyłem, po dalszych badaniach.

Mam dostawcę jako pojęcie, które implementuje AuthenticationProvider. Celowo nie jest to Fasola / Komponent. W przeciwnym razie Spring zarejestruje go jako jedynego dostawcę.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

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

W moim SecurityConfig zezwalam Springowi na automatyczne AuthenticationManagerBuilderuruchamianie i ręczne wstrzykiwanie mojegoMfaAuthenticationProvider

@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);
}

Po standardowym uwierzytelnieniu, jeśli użytkownik ma włączoną funkcję MFA, są one wstępnie uwierzytelniane z nadanym uprawnieniem PRE_AUTHENTICATED_MFA_REQUIRED . To pozwala im na dostęp do pojedynczego punktu końcowego, /auth/mfa-token. Ten punkt końcowy pobiera nazwę użytkownika z prawidłowego JWT i podanego TOTP i wysyła ją do authenticate()metody authenticationManagerBuilder, która wybiera MfaAuthenticationProvidersposób obsługiwania OneTimePasswordAuthenticationToken.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
phisch
źródło