Xác thực đa yếu tố với Spring Boot 2 và Spring Security 5


11

Tôi muốn thêm xác thực đa yếu tố với mã thông báo mềm TOTP vào ứng dụng Angular & Spring, đồng thời giữ mọi thứ gần nhất có thể với các mặc định của Spring Boot Security Starter .

Quá trình xác thực mã thông báo xảy ra cục bộ (với thư viện aerogear-otp-java), không có nhà cung cấp API bên thứ ba.

Thiết lập mã thông báo cho người dùng hoạt động, nhưng xác thực chúng bằng cách tận dụng Trình quản lý / nhà cung cấp xác thực bảo mật Spring thì không.

TL; DR

  • Cách chính thức để tích hợp một Trình xác thực bổ sung vào hệ thống được cấu hình Spring Boot Security Starter là gì?
  • Những cách được đề xuất để ngăn chặn các cuộc tấn công phát lại là gì?

Phiên bản dài

API có một điểm cuối /auth/tokenmà từ đó frontend có thể nhận được mã thông báo JWT bằng cách cung cấp tên người dùng và mật khẩu. Phản hồi cũng bao gồm trạng thái xác thực, có thể là AUTHENTICATED hoặc PRE_AUTHENTICATED_MFA_REQUIRED .

Nếu người dùng yêu cầu MFA, mã thông báo được cấp với một cơ quan được cấp duy nhất PRE_AUTHENTICATED_MFA_REQUIREDvà thời gian hết hạn là 5 phút. Điều này cho phép người dùng truy cập điểm cuối /auth/mfa-tokennơi họ có thể cung cấp mã TOTP từ ứng dụng Authenticator của họ và nhận mã thông báo được xác thực đầy đủ để truy cập trang web.

Nhà cung cấp và mã thông báo

Tôi đã tạo tùy chỉnh của mình MfaAuthenticationProvidermà thực hiện AuthenticationProvider:

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

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

Và một OneTimePasswordAuthenticationTokencái mở rộng AbstractAuthenticationTokenđể giữ tên người dùng (lấy từ JWT đã ký) và mã OTP.

Cấu hình

Tôi có tùy chỉnh của mình WebSecurityConfigurerAdapter, nơi tôi thêm tùy chỉnh của mình AuthenticationProviderthông qua http.authenticationProvider(). Tham gia vào JavaDoc, đây có vẻ là nơi thích hợp:

Cho phép thêm Trình xác thực bổ sung sẽ được sử dụng

Các phần có liên quan của tôi SecurityConfigtrông như thế này.

    @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

Bộ điều khiển

Các AuthControllercó các AuthenticationManagerBuildertiêm và kéo nó tất cả cùng nhau.

@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

Tuy nhiên, đăng bài chống lại /auth/mfa-tokendẫn đến lỗi này:

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

Tại sao Spring Security không nhận Nhà cung cấp xác thực của tôi? Gỡ lỗi bộ điều khiển cho tôi thấy đó DaoAuthenticationProviderlà Nhà cung cấp xác thực duy nhất trong AuthenticationProviderManager.

Nếu tôi trưng ra MfaAuthenticationProviderlà đậu, thì đó là Nhà cung cấp duy nhất được đăng ký, vì vậy tôi nhận được điều ngược lại:

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

Vì vậy, làm thế nào để tôi có được cả hai?

Câu hỏi của tôi

Cách được đề xuất để tích hợp bổ sung AuthenticationProvidervào hệ thống được cấu hình Spring Boot Security Starter để tôi có được cả DaoAuthenticationProvidertùy chỉnh và tùy chỉnh của riêng mình là MfaAuthenticationProvidergì? Tôi muốn giữ mặc định của Spring Boot Scurity Starter và có thêm Nhà cung cấp riêng.

Ngăn chặn tấn công Replay

Tôi biết rằng thuật toán OTP tự nó không bảo vệ chống lại các cuộc tấn công phát lại trong lát cắt thời gian trong đó mã hợp lệ; RFC 6238 làm rõ điều này

Trình xác minh PHẢI KHÔNG chấp nhận lần thử thứ hai của OTP sau khi xác thực thành công đã được ban hành cho OTP đầu tiên, điều này đảm bảo chỉ sử dụng một lần OTP.

Tôi đã tự hỏi nếu có một cách được đề nghị để thực hiện bảo vệ. Vì mã thông báo OTP dựa trên thời gian, tôi nghĩ đến việc lưu trữ lần đăng nhập thành công cuối cùng trên mô hình của người dùng và đảm bảo chỉ có một lần đăng nhập thành công trong mỗi lát thời gian 30 giây. Điều này tất nhiên có nghĩa là đồng bộ hóa trên mô hình người dùng. Bất kỳ phương pháp tốt hơn?

Cảm ơn bạn.

-

Tái bút: vì đây là câu hỏi về bảo mật nên tôi đang tìm câu trả lời rút ra từ các nguồn đáng tin cậy và / hoặc chính thức. Cảm ơn bạn.

Câu trả lời:


0

Để trả lời câu hỏi của riêng tôi, đây là cách tôi thực hiện nó, sau khi nghiên cứu thêm.

Tôi có một nhà cung cấp như một pojo thực hiện AuthenticationProvider. Nó cố tình không phải là Bean / Thành phần. Nếu không, Spring sẽ đăng ký nó như là Nhà cung cấp duy nhất.

public class MfaAuthenticationProvider implements AuthenticationProvider {
    private final AccountService accountService;

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

Trong SecurityConfig của tôi, tôi để Spring AuthenticationManagerBuildertự động và tự tiêmMfaAuthenticationProvider

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

Sau khi xác thực tiêu chuẩn, nếu người dùng đã bật MFA, họ sẽ được xác thực trước với quyền được cấp của PRE_AUTHENTICATED_MFA_REQUIRED . Điều này cho phép họ truy cập vào một điểm cuối duy nhất , /auth/mfa-token. Điểm cuối này lấy tên người dùng từ JWT hợp lệ và TOTP được cung cấp và gửi nó đến authenticate()phương thức của xác thực Manageragerager, lựa chọn MfaAuthenticationProvidernhư nó có thể xử lý OneTimePasswordAuthenticationToken.

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.