web-dev-qa-db-ja.com

Spring Boot 2およびSpring Security 5を使用した多要素認証

TOTPソフトトークンを使用した多要素認証をAngular&Springアプリケーションに追加し、すべてをデフォルトのSpring Bootにできるだけ近づけます)セキュリティスターター

トークンの検証はローカルで行われ(aerogear-otp-Javaライブラリを使用)、サードパーティのAPIプロバイダーは使用されません。

ユーザーのトークンの設定は機能しますが、Spring Security Authentication Manager/Providersを利用してトークンを検証することはできません。

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_にアクセスでき、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があります。ここで、http.authenticationProvider()を介してカスタム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として公開すると、登録されているのはonlyプロバイダーなので、反対の結果になります。

_No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken. 
_

それで、どうすれば両方を入手できますか?

私の質問

追加のAuthenticationProviderSpring Boot Security Starter構成されたシステムに統合して、両方を取得するための推奨される方法は何ですか、DaoAuthenticationProviderと自分のカスタムMfaAuthenticationProviderSpring Boot Scurity Starterのデフォルトを維持し、独自のプロバイダーを追加したいと思います。

リプレイ攻撃の防止

OTPアルゴリズム自体は、コードが有効なタイムスライス内のリプレイアタックから保護されないことを知っています。 RFC 6238はこれを明確にしています

検証者は、最初のOTPに対して検証が正常に発行された後、OTPの2回目の試行を受け入れてはいけません。

保護を実装するための推奨される方法があるかどうか疑問に思っていました。 OTPトークンは時間ベースなので、最後に成功したログインをユーザーのモデルに保存し、30秒のタイムスライスごとに1つのログインのみが成功することを確認します。もちろん、これはユーザーモデルの同期を意味します。より良いアプローチはありますか?

ありがとうございました。

-

PS:これはセキュリティに関する質問なので、信頼できる情報源や公式な情報源からの回答を探しています。ありがとうございました。

11
phisch

私自身の質問に答えるために、これは私がそれをさらに研究した後に実装した方法です。

AuthenticationProviderを実装するプロバイダーをpojoとして使用しています。意図的にBean /コンポーネントではありません。それ以外の場合、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からユーザー名を取得し、authenticationManagerBuilderのauthenticate()メソッドに送信します。authenticationManagerBuilderは、MfaAuthenticationProviderを処理できるため、OneTimePasswordAuthenticationTokenを選択します。

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