Otentikasi Multi-Faktor dengan Spring Boot 2 dan Spring Security 5

11

Saya ingin menambahkan otentikasi multi-faktor dengan token lunak TOTP ke aplikasi Angular & Spring, sambil menjaga semuanya sedekat mungkin dengan default dari Spring Boot Security Starter .

Validasi token terjadi secara lokal (dengan pustaka aerogear-otp-java), tidak ada penyedia API pihak ketiga.

Menyiapkan token untuk pengguna berfungsi, tetapi memvalidasinya dengan memanfaatkan Spring Security Authentication Manager / Penyedia tidak.

TL; DR

  • Apa cara resmi untuk mengintegrasikan AuthenticationProvider tambahan ke sistem terkonfigurasi Spring Boot Security Starter ?
  • Apa cara yang disarankan untuk mencegah serangan replay?

Versi Panjang

API memiliki titik akhir /auth/tokendari mana frontend bisa mendapatkan token JWT dengan memberikan nama pengguna dan kata sandi. Respons juga mencakup status otentikasi, yang dapat berupa AUTHENTICATED atau PRE_AUTHENTICATED_MFA_REQUIRED .

Jika pengguna membutuhkan MFA, token diberikan dengan otoritas tunggal PRE_AUTHENTICATED_MFA_REQUIREDdan waktu kedaluwarsa 5 menit. Ini memungkinkan pengguna untuk mengakses titik akhir di /auth/mfa-tokenmana mereka dapat memberikan kode TOTP dari aplikasi Authenticator mereka dan mendapatkan token yang sepenuhnya dikonfirmasi untuk mengakses situs.

Penyedia dan Token

Saya telah membuat kebiasaan saya MfaAuthenticationProvideryang mengimplementasikan AuthenticationProvider:

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

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

Dan OneTimePasswordAuthenticationTokenyang meluas AbstractAuthenticationTokenuntuk menampung nama pengguna (diambil dari JWT yang ditandatangani) dan kode OTP.

Konfigurasi

Saya memiliki kebiasaan saya WebSecurityConfigurerAdapter, tempat saya menambahkan kebiasaan saya AuthenticationProvidermelalui http.authenticationProvider(). Menurut JavaDoc, ini sepertinya tempat yang tepat:

Mengizinkan menambahkan AuthenticationProvider tambahan untuk digunakan

Bagian yang relevan dari SecurityConfigpenampilan saya seperti ini.

    @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

Pengendali

The AuthControllertelah yang AuthenticationManagerBuilderdisuntikkan dan menariknya semua bersama-sama.

@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

Namun, memposting yang /auth/mfa-tokenmengarah pada kesalahan ini:

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

Mengapa Spring Security tidak mengambil Penyedia Autentikasi saya? Debugging controller menunjukkan kepada saya bahwa itu DaoAuthenticationProvideradalah satu-satunya Penyedia Autentikasi di AuthenticationProviderManager.

Jika saya mengekspos MfaAuthenticationProviderkacang saya , itu adalah satu - satunya Penyedia yang terdaftar, jadi saya mendapatkan yang sebaliknya:

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

Jadi, bagaimana saya mendapatkan keduanya?

Pertanyaan saya

Apa cara yang disarankan untuk mengintegrasikan tambahan AuthenticationProvider ke sistem terkonfigurasi Spring Boot Security Starter , sehingga saya mendapatkan keduanya, DaoAuthenticationProviderkebiasaan dan kebiasaan saya sendiri MfaAuthenticationProvider? Saya ingin mempertahankan default Spring Boot Scurity Starter dan memiliki Penyedia sendiri.

Pencegahan Serangan Putar Ulang

Saya tahu bahwa algoritma OTP tidak dengan sendirinya melindungi terhadap serangan replay dalam slice waktu di mana kode tersebut valid; RFC 6238 memperjelas hal ini

Verifier TIDAK HARUS menerima upaya kedua OTP setelah validasi yang berhasil dikeluarkan untuk OTP pertama, yang memastikan hanya sekali pakai OTP.

Saya bertanya-tanya apakah ada cara yang disarankan untuk menerapkan perlindungan. Karena token OTP berbasis waktu, saya berpikir untuk menyimpan login yang berhasil terakhir pada model pengguna dan memastikan hanya ada satu login yang berhasil per 30 detik slice waktu. Ini tentu saja berarti sinkronisasi pada model pengguna. Adakah pendekatan yang lebih baik?

Terima kasih.

-

PS: karena ini pertanyaan tentang keamanan saya mencari jawaban dari sumber yang kredibel dan / atau resmi. Terima kasih.

phisch
sumber

Jawaban:

0

Untuk menjawab pertanyaan saya sendiri, ini adalah bagaimana saya menerapkannya, setelah penelitian lebih lanjut.

Saya memiliki penyedia sebagai pojo yang mengimplementasikan AuthenticationProvider. Sengaja bukan Kacang / Komponen. Kalau tidak, Spring akan mendaftarkannya sebagai satu-satunya Penyedia.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

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

Di SecurityConfig saya, saya membiarkan Spring autowire AuthenticationManagerBuilderdan menyuntikkan secara manualMfaAuthenticationProvider

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

Setelah otentikasi standar, jika pengguna telah mengaktifkan MFA, mereka telah disahkan sebelumnya dengan otoritas PRE_AUTHENTICATED_MFA_REQUIRED yang telah diberikan . Ini memungkinkan mereka untuk mengakses satu titik akhir,/auth/mfa-token ,. Titik akhir ini mengambil nama pengguna dari JWT yang valid dan TOTP yang disediakan dan mengirimkannya ke authenticate()metode otentikasiManagerBuilder, yang memilih yang MfaAuthenticationProviderdapat ditangani OneTimePasswordAuthenticationToken.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
phisch
sumber