SpringBoot Google Authenticator 実装 設定 2要素認証 MFA

今回は、Google Authenticatorを利用してMFA認証を実装するメモとなります。
SpringBootを利用したWebベースでの実装内容になっています。

ログイン後の画面に、QRコード表示を行い認証アプリで認証コードを取得できる様にして、
認証コードでの認証をできる様にする内容になっています。

https://github.com/wstrange/GoogleAuth/tree/master

検証環境・バージョン

  • MacOS:Sonoma 14.6
  • Amazon Corretto:17
  • SpringBoot:3.4.0
  • IntelliJ IDEA:2024.2 (Ultimate Edition)

ビルド設定(Gradle・Maven)

・Gradle

implementation 'com.warrenstrange:googleauth:1.4.0'

・Maven

<dependency>
    <groupId>com.warrenstrange</groupId>
    <artifactId>googleauth</artifactId>
    <version>1.4.0</version>
</dependency>

・build.gradle例

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.4.0'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'example.com'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'

    implementation 'com.warrenstrange:googleauth:1.4.0'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
    useJUnitPlatform()
}

実装

QRコード表示

・サーバーサイド

// Generate a secret key
final GoogleAuthenticatorKey key = gAuth.createCredentials();
// Generate QR code URL
final String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL("AppName", "AccountName", key);

// show the QR code URL
final ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("mfa_form");
modelAndView.addObject("qrCodeUrl", qrCodeUrl);

GoogleAuthenticatorKeyを作成し、GoogleAuthenticatorQRGenerator.getOtpAuthURL(String issuer, String accountName, GoogleAuthenticatorKey credentials)で、QRコードURLを作成します。
getOtpAuthURLの第一引数に指定した文字列が、認証アプリ上でアプリ名として表示され、
第二引数で指定された文字列がユーザ名として表示されます。

尚、もしもQRコード表示をせずにメール等で認証コードを送信したい場合、以下の要領で発行できます。

// This is a sample secret key. You should generate a random secret key.
final String fixedSecret = hardcodedUsername + "_01";
// Create a new secret key
final GoogleAuthenticatorKey key = new GoogleAuthenticatorKey.Builder(fixedSecret).build();
// Get the verification code
final int verificationCode = gAuth.getTotpPassword(fixedSecret);

・フロントエンド

<div>
    <p>QRコードをスキャンして認証コードを確認して認証して下さい。</p>
    <img th:src="@{${qrCodeUrl}}" alt="QR Code"/>
</div>

コード認証処理

・サーバーサイド

@PostMapping("/verifyMfa")
public ModelAndView verifyMfa(@RequestParam final String wfaCode, @RequestParam final String secretKey) {
    final ModelAndView modelAndView = new ModelAndView();
    final boolean isCodeValid = gAuth.authorize(secretKey, Integer.parseInt(wfaCode));
    if (isCodeValid) {
        modelAndView.setViewName("mfa_ok");
    } else {
        modelAndView.setViewName("mfa_form");
        // Generate QR code URL
        final GoogleAuthenticatorKey recreatedKey = new GoogleAuthenticatorKey.Builder(secretKey).build();
        final String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL("AppName", "AccountName",recreatedKey);
        modelAndView.addObject("qrCodeUrl", qrCodeUrl);
        modelAndView.addObject("secretKey", recreatedKey.getKey());
        modelAndView.addObject("error", "Invalid WFA key!");
    }
    return modelAndView;
}

GoogleAuthenticator().authorize(String secret, int verificationCode)で、認証コードチェックを行います。

・フロントエンド

<form th:action="@{/verifyMfa}" method="post">
    <label for="wfaCode">wfa key:</label>
    <input type="text" id="wfaCode" name="wfaCode"/>
    <input type="hidden" name="secretKey" th:value="${secretKey}"/>
    <button type="submit">key check</button>
</form>

・コントローラー全体像

package example.com.googleauthenticator.controller;

import com.warrenstrange.googleauth.GoogleAuthenticator;
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MfaSampleController {

    private final GoogleAuthenticator gAuth = new GoogleAuthenticator();

    @GetMapping("/")
    public ModelAndView index() {
        final ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("index");
        return modelAndView;
    }

    @PostMapping("/login")
    public ModelAndView login(@RequestParam final String id, @RequestParam final String password) {
        final ModelAndView modelAndView = new ModelAndView();
        final String hardcodedId = "sample";
        final String hardcodedPassword = "pass";
        if (id.equals(hardcodedId) && hardcodedPassword.equals(password)) {
            // Generate a secret key
            final GoogleAuthenticatorKey secretKey = gAuth.createCredentials();
            // Generate QR code URL
            final String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL("AppName", "AccountName", secretKey);
            // show the QR code URL
            modelAndView.setViewName("mfa_form");
            modelAndView.addObject("qrCodeUrl", qrCodeUrl);
            modelAndView.addObject("secretKey", secretKey.getKey());
        } else {
            modelAndView.setViewName("index");
            modelAndView.addObject("message", "Invalid password");
        }
        return modelAndView;
    }

    @PostMapping("/verifyMfa")
    public ModelAndView verifyMfa(@RequestParam final String wfaCode, @RequestParam final String secretKey) {
        final ModelAndView modelAndView = new ModelAndView();
        final boolean isCodeValid = gAuth.authorize(secretKey, Integer.parseInt(wfaCode));
        if (isCodeValid) {
            modelAndView.setViewName("mfa_ok");
        } else {
            modelAndView.setViewName("mfa_form");
            // Generate QR code URL
            final GoogleAuthenticatorKey recreatedKey = new GoogleAuthenticatorKey.Builder(secretKey).build();
            final String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL("AppName", "AccountName",recreatedKey);
            modelAndView.addObject("qrCodeUrl", qrCodeUrl);
            modelAndView.addObject("secretKey", recreatedKey.getKey());
            modelAndView.addObject("error", "Invalid WFA key!");
        }
        return modelAndView;
    }
}

・フロントエンド全体像

index.html ログイン画面
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Google Authenticator Test</title>
</head>
<body>
<h1>Google Authenticator Test</h1>

<form action="/login" method="post">
    ID: <input type="text" name="id" required><br>
    Password: <input type="password" name="password" required><br>
    <button type="submit">Login</button>
</form>

</body>
</html>
mfa_form.html MFA認証フォーム
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Google Authenticator Test</title>
</head>
<body>
<h1>Google Authenticator Test</h1>
<h2>MFA check</h2>
<div>
    <p>QRコードをスキャンして認証コードを確認して認証して下さい。</p>
    <img th:src="@{${qrCodeUrl}}" alt="QR Code" style="margin-left: 20px;"/>
    <p>
        <label>secretKey:</label><span th:text="${secretKey}"></span>
    </p>
</div>
<br>
<hr>

<th:block th:if="${error}">
    <div style="color: red;">認証コードが間違っています。</div>
</th:block>

<form th:action="@{/verifyMfa}" method="post">
    <label for="wfaCode">wfa key:</label>
    <input type="text" id="wfaCode" name="wfaCode"/>
    <input type="hidden" name="secretKey" th:value="${secretKey}"/>
    <button type="submit">key check</button>
</form>
</body>
</html>
mfa_ok.html MFA認証後画面
<html xmlns:th="https://www.thymeleaf.org">
<head>
    <title>Google Authenticator Test</title>
</head>
<body>
<h1>Google Authenticator Test</h1>
<h2>MFA check OK!!</h2>
<div>MFA認証が成功しました。</div>
</body>
</html>

補足

尚、シークレットキーを自身で決めたい場合や、
シークレットキーを指定してQRコードを作成したい場合は、
GoogleAuthenticatorKey.Builder(String value)の引数に指定することで対応できます。

// Generate a secret key
final GoogleAuthenticatorKey secretKey = new GoogleAuthenticatorKey.Builder(id).build();
// Generate QR code URL
final String qrCodeUrl = GoogleAuthenticatorQRGenerator.getOtpAuthURL("AppName", "AccountName", secretKey);

https://github.com/wstrange/GoogleAuth/blob/master/src/main/java/com/warrenstrange/googleauth/GoogleAuthenticator.java


挙動確認

簡易的なログインを行います。

画面に表示したQRコードを認証アプリで読み込み、
確認した認証コードでチェックを行います。

認証キーが正しく有効であれば、画面遷移します。


ライセンスについて

GoogleAuthenticatorは、現時点では三条項BSDライセンス(修正BSDライセンス)です。
使用する際にはライセンス変更がないか確認してください。

https://github.com/wstrange/GoogleAuth/blob/master/LICENSE

商用利用可能ですが原本に記載されている通り、使用する場合以下を守る必要があります。

  • ソースコードを再頒布する場合、上記の著作権表示、本条件一覧、および下記免責条項を含めること。
  • バイナリ形式で再頒布する場合、頒布物に付属のドキュメント等の資料に、
    上記の著作権表示、本条件一覧、および下記免責条項を含めること。
  • 書面による特別の許可なしに、本ソフトウェアから派生した製品の宣伝または販売促進に、
    <組織> の名前またはコントリビューターの名前を使用してはならない。

原本


今回のメモは以上です。
GoogleAuthenticatorはインフラなどの用意も必要なく実装できるため、非常に便利です。

ライセンスも緩く商用利用可能な制限なので、
色々な案件で利用できると思います。

都内でエンジニアをやっています。 2017年に脱サラ(法人設立)しました。 仕事で調べたことや、気になったことをメモしています。
投稿を作成しました 173

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


関連投稿

検索語を上に入力し、 Enter キーを押して検索します。キャンセルするには ESC を押してください。

トップに戻る