🐈

Spring Security 6.4.0-RC1 でパスキー認証を実装してみる

2024/11/03に公開

はじめに

Spring Security 6.4.0-RC1 で パスキー対応したということで、どんなものか使ってみたメモです。

https://spring.io/blog/2024/10/21/spring-security-6-4-0-rc1-is-available-now

https://docs.spring.io/spring-security/reference/6.4-SNAPSHOT/servlet/authentication/passkeys.html

環境

  • macOS Sonoma 14.6
  • Chrome 130
  • Spring Boot 3.4.0
  • Spring Security 6.4.0 RC1
  • Kotlin, Java, Java Script

サンプルアプリ

サンプルアプリはSpring Bootアプリです。
spring initializr で以下の設定で作ったプロジェクトです。

Project: Gradle-Kotlin
Language: Kotlin
Spring Boot: 3.4.0(SNAPSHOT)
Packaging: Jar
Java: 17
Dependencies
- Spring Web
- Thymeleaf
- Spring Security
- Spring Data JPA
- H2 Database
- Spring Boot DevTools

実装前

  • IDとパスワードでログインするだけのシンプルなものです。

    • user1/password1
    • user2/password2
    • user3/password3
  • アカウントデータは永続化したH2データベースに持っているんでアプリを再起動しても消えることはありません。DB接続情報は application.properties を参照してください。

このサンプルアプリにパスキー認証を追加します。

パスキー認証実装の概要

最初に超ざっくりまとめるとこんな感じです。

  • 依存関係、Configuration、テーブル追加する
  • フロントのJavaScriptは SimpleWebAuthn ライブラリを使う
  • サーバーサイドのSpring Securityは
    • PublicKeyCredentialUserEntityRepository インタフェースを実装したBeanを作成する
    • UserCredentialRepository インタフェースを実装したBeanを作成する
  • パスキー作成のエンドポイントとFilter
    • POST /webauthn/register/optionsPublicKeyCredentialCreationOptionsFilter
    • POST /webauthn/registerWebAuthnRegistrationFilter
  • パスキー認証のエンドポイントとFilter
    • POST /webauthn/authenticate/optionsPublicKeyCredentialRequestOptionsFilter
    • POST /login/webauthnWebAuthnAuthenticationFilter

実装1 - 基本設定的なこと

依存関係

依存関係に webauthn4j-core を追加する必要があります。

build.gradle.kts

dependencies {
	implementation("org.springframework.boot:spring-boot-starter-data-jpa")
	implementation("org.springframework.boot:spring-boot-starter-security")
	implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
	implementation("org.springframework.boot:spring-boot-starter-web")
	implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
	implementation("org.jetbrains.kotlin:kotlin-reflect")
	implementation("org.thymeleaf.extras:thymeleaf-extras-springsecurity6")
	implementation("com.webauthn4j:webauthn4j-core:0.27.0.RELEASE") ← これを追加
	developmentOnly("org.springframework.boot:spring-boot-devtools")
	runtimeOnly("com.h2database:h2")
	testImplementation("org.springframework.boot:spring-boot-starter-test")
	testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
	testImplementation("org.springframework.security:spring-security-test")
	testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

Configuration

HttpSecurity の設定に webAuthn を追加します。

  • rpName: サービス名です。任意の文字列でいいと思います。
  • rpId: サービスのドメインを指定します。今回はローカルで実行するサンプルなので localhost にします。
  • allowedOrigins: パスキーが有効なドメインを指定します。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/config/WebSecurityConfig.kt#L13-L32

テーブル追加

パスキーを保存するテーブルを作成します。

create table M_PASSKEY_CREDENTIAL (
    ID int default 0 not null auto_increment primary key,
    CREDENTIAL_ID varbinary not null unique,
    USER_INTERNAL_ID varchar not null,
    PUBLIC_KEY varbinary,
    ATTESTED_CREDENTIAL_DATA_JSON varbinary,
    ATTESTATION_OBJECT varbinary
);

実装2 - パスキー作成

パスキー作成画面

Create a passkey ボタンを追加します。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/templates/mypage.html#L16

パスキー作成処理

Create a passkey ボタンクリックしたらパスキー作成をします。

パスキー作成のオプション取得(JavaScript)

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/static/js/index.js#L12-L27

パスキー作成のオプション取得(Spring Security)

ここで、実装が必要になります。パスキーを作成するオプションには以下の情報が必要です。

  • rp: Configuration で設定したものが利用されます。
  • user: パスキーを作成するユーザーを指定します。
  • excludeCredentials: 対象のユーザーで作成済みのパスキー情報を指定します。
  • それ以外のいろんなオプションは Spring Security が自動で作ってくれます。

オプションデータはたくさんあるんですが、ほとんどはSpring Security が自動で作ってくれます。ここでは user, excludeCredentials に設定するデータを作る必要があります。

user: パスキーを作成するアカウントを求める

PublicKeyCredentialUserEntityRepository インタフェースを実装したBeanを作って対応する必要があります。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/PublicKeyCredentialUserEntityRepositoryImpl.kt#L11-L14

findByUsername() を実装します。引数にユーザーが指定されるので、アカウントマスタから PublicKeyCredentialUserEntity を生成して返却します。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/PublicKeyCredentialUserEntityRepositoryImpl.kt#L15-L24

excludeCredentials: 対象のユーザーで作成済みのパスキー情報を求める

UserCredentialRepository インタフェースを実装したBeanを作って対応する必要があります。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/UserCredentialRepositoryImpl.kt#L12-L15

findByUserId() を実装します。引数にUserEntity の idが指定されてくるので、パスキークレデンシャルマスタから PublicKeyCredentialUserEntity を生成して返却します。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/UserCredentialRepositoryImpl.kt#L55-L60

パスキー作成(JavaScript)

SimpleWebAuthnSimpleWebAuthnBrowser.startRegistration を使います。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/static/js/index.js#L29-L44

パスキー保存(JavaScript)

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/static/js/index.js#L46-L72

保存が正常終了すると以下のJSONが帰ってきます。

HTTP/1.1 200 OK

{
  "success": true
}

パスキー保存(Spring Security)

ここで、実装が必要になります。

  • 未登録のパスキークレデンシャルかどうか確認する
  • DBにパスキークレデンシャルを保存する

未登録のパスキークレデンシャルかどうか確認する

UserCredentialRepository インタフェースを実装したBeanで対応します。

findByCredentialId() を実装します。引数に credentialId が指定されてくるので、自アプリのパスキークレデンシャルマスタに未登録かどうか確認します。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/UserCredentialRepositoryImpl.kt#L49-L53

DBにパスキークレデンシャルを保存する

こちらも、UserCredentialRepository インタフェースを実装したBeanで対応します。

save() を実装します。引数で指定された CredentialRecord をDBに保存しましょう。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/UserCredentialRepositoryImpl.kt#L17-L47

実装3 - パスキー認証

ログイン画面

Signin with Passkey ボタンを追加します。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/templates/login.html#L24

パスキー認証処理

Signin with Passkey ボタンクリックしたらパスキー認証をします。

パスキー認証のオプション取得(JavaScript)

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/static/js/index.js#L81-L96

パスキー認証のオプション取得(Spring Security)

ここでは先程実装した PublicKeyCredentialUserEntityRepository#findByUsername がコールされますが、認証前のため、usernameが anonymousUser となり、空の UserEntity が返却され、正常終了します。

あとは自動で Spring Security が処理してくれます。

パスキー認証(JavaScript)

SimpleWebAuthnSimpleWebAuthnBrowser.startAuthentication を使います。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/static/js/index.js#L98-L105

ユーザーVerify(JavaScript)

Verifyが正常終了すると以下のJSONが帰ってきます。redirectUrl にリダイレクトしましょう。

HTTP/1.1 200 OK

{
  "redirectUrl": "/", 
  "authenticated": true 
}

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/resources/static/js/index.js#L107-L134

ユーザーVeirfy(Spring Security)

ここでは以下の順に処理が実行されます。

  1. UserCredentialRepository#findByCredentialId

    • クレデンシャルIDに紐づくクレデンシャルデータ(公開鍵など)を返却します。
  2. Verify

    • Spring Securityによって実施される認証の処理です。

    • Verifyが成功したら以降の処理を実施します。

  3. UserCredentialRepository#save

    • Verifyが成功したらコールされます。

    • CredentialRecord の以下情報が更新される場合があるので、本当はDBに保存したほうがいいんですけど、このサンプルアプリでは保存していません。

      • signatureCount
      • backupEligible
      • lastUsed
  4. PublicKeyCredentialUserEntityRepository#findById

    • ログインするユーザーアカウント情報を求めます。

ログインするユーザーアカウント情報を求める

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/webauthn/PublicKeyCredentialUserEntityRepositoryImpl.kt#L26-L32

ここで返却されるユーザー情報が Spring Securityの SecurityContext にセットされます。

ログインが完了した後は、以下のようにしてログインユーザーをGetしてます。

https://github.com/gebogebogebo/spring-security-6-4-passkey/blob/657a19d081feed5fd0b43552093a3332051b99be/src/main/kotlin/com/example/demo/controller/LoginController.kt#L56-L68

まとめ

ソースコードは こちら です。

かなり簡単にパスキー認証のサーバーサイドが実装できる、と感じました。

が、aaguidが取得できないなど必要な改善点はある、と感じました。

細かくアレコレカスタマイズしたい場合は、本家の webauthn4j を使ったほうがいいかもしれません。

おつかれさまでした

Discussion