13

我想向 Angular 和 Spring 应用程序添加使用 TOTP 软令牌的多因素身份验证,同时使所有内容尽可能接近Spring Boot Security Starter的默认值。

令牌验证在本地进行(使用 aerogear-otp-java 库),没有第三方 API 提供者。

为用户设置令牌是可行的,但通过利用 Spring Security Authentication Manager/Providers 来验证它们却不行。

TL;博士

  • 将额外的 AuthenticationProvider 集成到Spring Boot Security Starter配置的系统中的官方方法是什么?
  • 有哪些推荐的方法来防止重放攻击?

长版

API 有一个端点/auth/token,前端可以通过提供用户名和密码从该端点获取 JWT 令牌。响应还包括身份验证状态,可以是AUTHENTICATEDPRE_AUTHENTICATED_MFA_REQUIRED

如果用户需要 MFA,则颁发令牌时授予单一授权,PRE_AUTHENTICATED_MFA_REQUIRED有效期为 5 分钟。这允许用户访问端点/auth/mfa-token,他们可以从他们的 Authenticator 应用程序中提供 TOTP 代码并获取完全经过身份验证的令牌以访问该站点。

提供者和令牌

我创建了我的自定义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 不选择我的身份验证提供程序?调试控制器向我展示了它DaoAuthenticationProviderAuthenticationProviderManager.

如果我公开我MfaAuthenticationProvider的 bean,它是唯一注册的提供者,所以我得到相反的结果:

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

那么,我如何获得两者?

我的问题

将附加组件集成AuthenticationProviderSpring Boot Security Starter配置系统中的推荐方法是什么,以便我同时获得DaoAuthenticationProvider和我自己的自定义MfaAuthenticationProvider?我想保留Spring Boot Scurity Starter的默认值并另外拥有自己的 Provider。

防止重放攻击

我知道 OTP 算法本身并不能在代码有效的时间片内防止重放攻击;RFC 6238 清楚地说明了这一点

在对第一个 OTP 发出成功验证后,验证者不得接受 OTP 的第二次尝试,这确保一次性使用 OTP。

我想知道是否有推荐的方法来实施保护。由于 OTP 令牌是基于时间的,我正在考虑将最后一次成功登录存储在用户模型上,并确保每 30 秒时间片只有一次成功登录。这当然意味着用户模型上的同步。有更好的方法吗?

谢谢你。

--

PS:由于这是一个关于安全的问题,我正在寻找来自可靠和/或官方来源的答案。谢谢你。

4

1 回答 1

6

为了回答我自己的问题,经过进一步研究,这就是我实施它的方式。

我有一个提供者作为实现AuthenticationProvider. 它故意不是 Bean/组件。否则 Spring 会将其注册为唯一的 Provider。

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 的方法,该方法选择MfaAuthenticationProvideras it can handle OneTimePasswordAuthenticationToken

    var authenticationToken = new OneTimePasswordAuthenticationToken(usernameFromJwt, providedOtp);
    var authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
于 2020-02-18T12:49:53.423 回答