Spring Securityとの二要素認証

1概要

このチュートリアルでは、https://en.wikipedia.org/wiki/Multi-factor__authentication[2要素認証機能]をソフトトークンとSpring Securityで実装します。

既存の単純なログインフロー に新しい機能を追加し、https://play.google.com/store/appsを使用します/details?id=com.google.android.apps.authenticator2

簡単に言えば、2要素認証は「ユーザーが知っていることとユーザーが持っていること」というよく知られた原則に従う検証プロセスです。

そのため、認証時にユーザーは追加の「検証トークン」を提供します。これは、時間ベースのワンタイムパスワードhttps://tools.ietf.org/html/rfc6238[TOTP]アルゴリズムに基づくワンタイムパスワード検証コードです。

2 Mavenの設定

まず、Google Authenticatorをアプリで使用するには、次のことが必要です。

  • 秘密鍵を生成する

  • QRコードを介してユーザーに秘密鍵を提供する

  • この秘密鍵を使用してユーザーが入力したトークンを確認してください。

次の依存関係を pom.xml に追加して、ワンタイムパスワードを生成/確認するために、単純なサーバーサイドhttps://github.com/aerogear/aerogear-otp-java[library]を使用します。

<dependency>
    <groupId>org.jboss.aerogear</groupId>
    <artifactId>aerogear-otp-java</artifactId>
    <version>1.0.0</version>
</dependency>

3ユーザーエンティティ

次に、追加情報を保持するようにユーザーエンティティを変更します。

@Entity
public class User {
    ...
    private boolean isUsing2FA;
    private String secret;

    public User() {
        super();
        this.secret = Base32.random();
        ...
    }
}

ご了承ください:

  • 後で使用できるように、各ユーザーのランダムな秘密コードを保存

確認コードの生成 ** 2段階認証プロセスはオプションです

4追加ログインパラメータ

まず、追加のパラメータ検証トークンを受け入れるようにセキュリティ構成を調整する必要があります。これを実現するには、カスタムの AuthenticationDetailsS​​ource を使用します。

これが CustomWebAuthenticationDetailsS​​ource です。

@Component
public class CustomWebAuthenticationDetailsSource implements
  AuthenticationDetailsSource<HttpServletRequest, WebAuthenticationDetails> {

    @Override
    public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
        return new CustomWebAuthenticationDetails(context);
    }
}

これが CustomWebAuthenticationDetails です。

public class CustomWebAuthenticationDetails extends WebAuthenticationDetails {

    private String verificationCode;

    public CustomWebAuthenticationDetails(HttpServletRequest request) {
        super(request);
        verificationCode = request.getParameter("code");
    }

    public String getVerificationCode() {
        return verificationCode;
    }
}

そして私達のセキュリティ設定:

@Configuration
@EnableWebSecurity
public class LssSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomWebAuthenticationDetailsSource authenticationDetailsSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
            .authenticationDetailsSource(authenticationDetailsSource)
            ...
    }
}

そして最後に、ログインフォームに追加のパラメータを追加します。

<labelth:text="#{label.form.login2fa}">
    Google Authenticator Verification Code
</label>
<input type='text' name='code'/>

注:セキュリティ設定でカスタムの AuthenticationDetailsS​​ource を設定する必要があります。

5カスタム認証プロバイダ

次に、追加のパラメータ検証を処理するためのカスタム AuthenticationProvider が必要です。

public class CustomAuthenticationProvider extends DaoAuthenticationProvider {

    @Autowired
    private UserRepository userRepository;

    @Override
    public Authentication authenticate(Authentication auth)
      throws AuthenticationException {
        String verificationCode
          = ((CustomWebAuthenticationDetails) auth.getDetails())
            .getVerificationCode();
        User user = userRepository.findByEmail(auth.getName());
        if ((user == null)) {
            throw new BadCredentialsException("Invalid username or password");
        }
        if (user.isUsing2FA()) {
            Totp totp = new Totp(user.getSecret());
            if (!isValidLong(verificationCode) || !totp.verify(verificationCode)) {
                throw new BadCredentialsException("Invalid verfication code");
            }
        }

        Authentication result = super.authenticate(auth);
        return new UsernamePasswordAuthenticationToken(
          user, result.getCredentials(), result.getAuthorities());
    }

    private boolean isValidLong(String code) {
        try {
            Long.parseLong(code);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

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

注意してください - ワンタイムパスワード確認コードを確認した後に、認証ダウンストリームを単に委任しました。

これが認証プロバイダBeanです。

@Bean
public DaoAuthenticationProvider authProvider() {
    CustomAuthenticationProvider authProvider = new CustomAuthenticationProvider();
    authProvider.setUserDetailsService(userDetailsService);
    authProvider.setPasswordEncoder(encoder());
    return authProvider;
}

6. 登録手続き

現在、ユーザーがアプリケーションを使用してトークンを生成できるようにするには、登録時に適切に設定する必要があります。

そのため、2段階認証プロセスを使用して後からログインする必要のあるQRコードをスキャンすることを選択したユーザーを許可するために、登録プロセスに簡単な変更を加える必要があります。

まず、この簡単な入力を登録フォームに追加します。

Use Two step verification <input type="checkbox" name="using2FA" value="true"/>

次に、 RegistrationController で、登録を確認した後、ユーザーの選択に基づいてユーザーをリダイレクトします。

@RequestMapping(value = "/registrationConfirm", method = RequestMethod.GET)
public String confirmRegistration(@RequestParam("token") String token, ...) {
    String result = userService.validateVerificationToken(token);
    if(result.equals("valid")) {
        User user = userService.getUser(token);
        if (user.isUsing2FA()) {
            model.addAttribute("qr", userService.generateQRUrl(user));
            return "redirect:/qrcode.html?lang=" + locale.getLanguage();
        }

        model.addAttribute(
          "message", messages.getMessage("message.accountVerified", null, locale));
        return "redirect:/login?lang=" + locale.getLanguage();
    }
    ...
}

そして、これが私たちのメソッド generateQRUrl() です:

public static String QR__PREFIX =
  "https://chart.googleapis.com/chart?chs=200x200&chld=M%%7C0&cht=qr&chl=";

@Override
public String generateQRUrl(User user) {
    return QR__PREFIX + URLEncoder.encode(String.format(
      "otpauth://totp/%s:%s?secret=%s&issuer=%s",
      APP__NAME, user.getEmail(), user.getSecret(), APP__NAME),
      "UTF-8");
}

そして、これが私たちの qrcode.html です。

<html>
<body>
<div id="qr">
    <p>
        Scan this Barcode using Google Authenticator app on your phone
        to use it later in login
    </p>
    <img th:src="${param.qr[0]}"/>
</div>
<a href="/login" class="btn btn-primary">Go to login page</a>
</body>
</html>

ご了承ください:

  • generateQRUrl() メソッドはQRコードURLを生成するために使用されます

  • このQRコードは、Googleを使用しているユーザーの携帯電話によってスキャンされます。

認証アプリ ** アプリはわずか30秒間有効な6桁のコードを生成します

これは望ましい検証コードです ** この確認コードは、当社のカスタムを使用してログイン中に確認されます

認証プロバイダ

7. 2段階認証を有効にする

次に、次のように、ユーザーがいつでも自分のログイン設定を変更できるようにします。

@RequestMapping(value = "/user/update/2fa", method = RequestMethod.POST)
@ResponseBody
public GenericResponse modifyUser2FA(@RequestParam("use2FA") boolean use2FA)
  throws UnsupportedEncodingException {
    User user = userService.updateUser2FA(use2FA);
    if (use2FA) {
        return new GenericResponse(userService.generateQRUrl(user));
    }
    return null;
}

そしてこれが updateUser2FA() です。

@Override
public User updateUser2FA(boolean use2FA) {
    Authentication curAuth = SecurityContextHolder.getContext().getAuthentication();
    User currentUser = (User) curAuth.getPrincipal();
    currentUser.setUsing2FA(use2FA);
    currentUser = repository.save(currentUser);

    Authentication auth = new UsernamePasswordAuthenticationToken(
      currentUser, currentUser.getPassword(), curAuth.getAuthorities());
    SecurityContextHolder.getContext().setAuthentication(auth);
    return currentUser;
}

そして、これがフロントエンドです。

<div th:if="${#authentication.principal.using2FA}">
    You are using Two-step authentication
    <a href="#" onclick="disable2FA()">Disable 2FA</a>
</div>
<div th:if="${! #authentication.principal.using2FA}">
    You are not using Two-step authentication
    <a href="#" onclick="enable2FA()">Enable 2FA</a>
</div>
<br/>
<div id="qr" style="display:none;">
    <p>Scan this Barcode using Google Authenticator app on your phone </p>
</div>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.2/jquery.min.js"></script>
<script type="text/javascript">
function enable2FA(){
    set2FA(true);
}
function disable2FA(){
    set2FA(false);
}
function set2FA(use2FA){
    $.post( "/user/update/2fa", { use2FA: use2FA } , function( data ) {
        if(use2FA){
            $("#qr").append('<img src="'+data.message+'"/>').show();
        }else{
            window.location.reload();
        }
    });
}
</script>

8結論

このクイックチュートリアルでは、Spring Securityでソフトトークンを使用して2要素認証を実装する方法を説明しました。

完全なソースコードは - いつも通り - over on GitHub にあります。