パスワード保存

Spring Security の PasswordEncoder インターフェースは、パスワードの一方向の変換を実行して、パスワードを安全に保存できるようにするために使用されます。PasswordEncoder は一方向の変換であるため、パスワード変換を双方向にする必要がある場合(データベースへの認証に使用される資格情報の保存など)には役立ちません。通常、PasswordEncoder は、認証時にユーザーが提供したパスワードと比較する必要があるパスワードを保存するために使用されます。

パスワード保存履歴

何年にもわたって、パスワードを保存するための標準的なメカニズムが進化してきました。当初、パスワードはプレーンテキストで保存されていました。データストアにアクセスするために必要な資格情報にパスワードが保存されているため、パスワードは安全であると見なされました。ただし、悪意のあるユーザーは、SQL インジェクションなどの攻撃を使用して、ユーザー名とパスワードの大規模な「データダンプ」を取得する方法を見つけることができました。ますます多くのユーザー資格情報が公開されるにつれて、セキュリティの専門家は、ユーザーのパスワードを保護するためにさらに多くのことを行う必要があることに気づきました。

その後、開発者は、SHA-256 などの一方向ハッシュを介してパスワードを実行した後にパスワードを保存するように促されました。ユーザーが認証を試みたとき、ハッシュされたパスワードは、ユーザーが入力したパスワードのハッシュと比較されます。これは、システムがパスワードの一方向のハッシュを保存するだけでよいことを意味しました。違反が発生した場合、パスワードの一方向ハッシュのみが公開されました。ハッシュは一方向であり、ハッシュが与えられたパスワードを推測することは計算上困難であったため、システム内の各パスワードを把握するために努力する価値はありません。この新しいシステムを打ち負かすために、悪意のあるユーザーはレインボーテーブル [Wikipedia] と呼ばれるルックアップテーブルを作成することにしました。毎回各パスワードを推測する作業を行うのではなく、パスワードを 1 回計算して、ルックアップテーブルに保存しました。

レインボーテーブルの効果を軽減するために、開発者はソルトされたパスワードを使用することが推奨されました。ハッシュ関数への入力としてパスワードだけを使用する代わりに、すべてのユーザーのパスワードに対してランダムなバイト(ソルトと呼ばれる)が生成されます。ソルトとユーザーのパスワードは、ハッシュ関数を介して実行され、一意のハッシュを生成します。ソルトは、ユーザーのパスワードと一緒にクリアテキストで保存されます。次に、ユーザーが認証を試みたときに、ハッシュされたパスワードが、保存されているソルトのハッシュおよびユーザーが入力したパスワードと比較されます。ユニークなソルトは、ソルトとパスワードの組み合わせごとにハッシュが異なるため、レインボーテーブルが効果的でなくなったことを意味します。

現代では、暗号化ハッシュ(SHA-256 など)はもはや安全ではないことがわかります。その理由は、最新のハードウェアでは、1 秒間に数十億のハッシュ計算を実行できるためです。これは、各パスワードを簡単に個別に解読できることを意味します。

開発者は現在、適応型一方向性関数を利用してパスワードを保存することが推奨されています。適応型一方向性関数を使用したパスワードの検証は、意図的にリソースを大量に消費します(意図的に大量の CPU、メモリ、その他のリソースを使用します)。適応型一方向性関数を使用すると、ハードウェアが向上するにつれて大きくなる可能性のある「作業要素」を構成できます。システムのパスワードを確認するために、「作業係数」を約 1 秒かかるように調整することをお勧めします。このトレードオフは、攻撃者がパスワードを解読することを困難にすることですが、システムに過度の負担をかけたり、ユーザーを苛立たせたりするほどのコストはかかりません。Spring Security は「作業係数」の良い出発点を提供しようとしましたが、パフォーマンスはシステムごとに大幅に異なるため、ユーザーは自分のシステムの「作業係数」をカスタマイズすることをお勧めします。使用する必要のある適応型一方向性関数の例には、bcryptPBKDF2scrypt、および argon2 が含まれます。

アダプティブ一方向性関数は意図的にリソースを大量に消費するため、すべてのリクエストに対してユーザー名とパスワードを検証すると、アプリケーションのパフォーマンスが大幅に低下する可能性があります。検証リソースを集中的に使用することでセキュリティが確保されるため、Spring Security(または他のライブラリ)がパスワードの検証を高速化するためにできることは何もありません。ユーザーは、長期のクレデンシャル(つまり、ユーザー名とパスワード)を短期のクレデンシャル(セッション、OAuth トークンなど)と交換することをお勧めします。短期間のクレデンシャルは、セキュリティを損なうことなく迅速に検証できます。

DelegatingPasswordEncoder

Spring Security 5.0 より前のデフォルトの PasswordEncoder は NoOpPasswordEncoder であり、プレーンテキストのパスワードが必要でした。パスワード履歴セクションに基づいて、デフォルトの PasswordEncoder が BCryptPasswordEncoder のようになると予想する場合があります。ただし、これは 3 つの現実世界の問題を無視します。

  • 多くのアプリケーションは、簡単に移行できない古いパスワードエンコーディングを使用しています。

  • パスワード保存のベストプラクティスは再び変更されます。

  • フレームワークとして、Spring Security は頻繁に重大な変更を加えることはできません。

代わりに、Spring Security は DelegatingPasswordEncoder を導入します。これは、次の方法ですべての問題を解決します。

  • 現在のパスワードストレージの推奨事項を使用してパスワードがエンコードされていることを確認する

  • 最新およびレガシー形式のパスワードの検証を許可する

  • 将来的にエンコーディングをアップグレードできるようにする

PasswordEncoderFactories を使用すると、DelegatingPasswordEncoder のインスタンスを簡単に作成できます。

デフォルトの DelegatingPasswordEncoder を作成
  • Java

  • Kotlin

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()

または、独自のカスタムインスタンスを作成することもできます。

カスタム DelegatingPasswordEncoder を作成
  • Java

  • Kotlin

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["sha256"] = StandardPasswordEncoder()

val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)

パスワード保存形式

パスワードの一般的な形式は次のとおりです。

DelegatingPasswordEncoder ストレージ形式
{id}encodedPassword

id は、使用する PasswordEncoder を検索するために使用される識別子であり、encodedPassword は、選択した PasswordEncoder の元のエンコードされたパスワードです。id は、パスワードの先頭にあり、{ で始まり、} で終わる必要があります。id が見つからない場合、id は null に設定されます。例: 以下は、さまざまな id 値を使用してエンコードされたパスワードのリストである可能性があります。元のパスワードはすべて password です。

DelegatingPasswordEncoder エンコードされたパスワードの例
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
1 最初のパスワードの PasswordEncoder ID は bcrypt で、encodedPassword 値は $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG です。マッチングすると、BCryptPasswordEncoder に委譲されます
22 番目のパスワードの PasswordEncoder ID は noop で、encodedPassword 値は password です。マッチングすると、NoOpPasswordEncoder に委譲されます
33 番目のパスワードの PasswordEncoder ID は pbkdf2 で、encodedPassword 値は 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc です。マッチングすると、Pbkdf2PasswordEncoder に委譲されます
44 番目のパスワードの PasswordEncoder ID は scrypt であり、encodedPassword 値は $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= です。一致すると、SCryptPasswordEncoder に委譲されます。
5 最終パスワードの PasswordEncoder ID は sha256 で、encodedPassword 値は 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 です。マッチングすると、StandardPasswordEncoder に委譲されます

ユーザーの中には、潜在的なハッカーのために保存形式が提供されていることを懸念する人もいるかもしれません。パスワードの保存は、アルゴリズムが秘密であることに依存していないため、これは心配ありません。さらに、ほとんどの形式は、接頭辞がなくても攻撃者が容易に理解できるものです。例: BCrypt のパスワードは、しばしば $2a$ で始まります。

パスワードエンコーディング

コンストラクターに渡された idForEncode は、パスワードのエンコードに使用される PasswordEncoder を決定します。以前に作成した DelegatingPasswordEncoder では、password のエンコード結果が BCryptPasswordEncoder に委譲され、接頭辞が {bcrypt} になることを意味します。最終結果は次の例のようになります。

DelegatingPasswordEncoder エンコードの例
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

パスワード照合

マッチングは、{id} と、コンストラクターで提供される id から PasswordEncoder へのマッピングに基づいています。パスワード保存形式の例は、これがどのように行われるかの実際の例を提供します。デフォルトでは、パスワードとマップされていない id (null ID を含む)を使用して matches(CharSequence, String) を呼び出した結果、IllegalArgumentException になります。この動作は、DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) を使用してカスタマイズできます。

id を使用することにより、任意のパスワードエンコーディングに一致させることができますが、最新のパスワードエンコーディングを使用してパスワードをエンコードします。暗号化とは異なり、パスワードハッシュはプレーンテキストを復元する簡単な方法がないように設計されているため、これは重要です。平文を復元する方法がないため、パスワードを移行することは困難です。ユーザーが NoOpPasswordEncoder を移行するのは簡単ですが、初心者向けのエクスペリエンスを簡単にするために、デフォルトで NoOpPasswordEncoder を含めることを選択しました。

はじめに

デモやサンプルをまとめる場合、ユーザーのパスワードをハッシュするのに時間がかかるのは少し面倒です。これを簡単にする便利なメカニズムがありますが、これはまだ本番用ではありません。

withDefaultPasswordEncoder の例
  • Java

  • Kotlin

UserDetails user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
val user = User.withDefaultPasswordEncoder()
    .username("user")
    .password("password")
    .roles("user")
    .build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

複数のユーザーを作成している場合は、ビルダーを再利用することもできます。

withDefaultPasswordEncoder ビルダーの再利用
  • Java

  • Kotlin

UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
UserDetails admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();
val users = User.withDefaultPasswordEncoder()
val user = users
    .username("user")
    .password("password")
    .roles("USER")
    .build()
val admin = users
    .username("admin")
    .password("password")
    .roles("USER", "ADMIN")
    .build()

これにより、保存されているパスワードがハッシュされますが、パスワードはメモリおよびコンパイルされたソースコードで公開されます。本番環境ではまだ安全とは見なされません。本番環境では、パスワードを外部でハッシュする必要があります

Spring Boot CLI でエンコードする

パスワードを適切にエンコードする最も簡単な方法は、Spring Boot CLI を使用することです。

例: 次の例では、DelegatingPasswordEncoder で使用するために password のパスワードをエンコードしています。

Spring Boot CLI encodepassword の例
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6

トラブルシューティング

パスワード保存形式に従って、保存されているパスワードの 1 つに id がない場合、次のエラーが発生します。

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

これを解決する最も簡単な方法は、パスワードが現在どのように保存されているかを把握し、正しい PasswordEncoder を明示的に提供することです。

Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder Bean を公開することにより、以前の動作に戻すことができます。

または、すべてのパスワードの前に正しい id を付けて、引き続き DelegatingPasswordEncoder を使用することもできます。例: BCrypt を使用している場合は、次のようなものからパスワードを移行します。

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

to

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

マッピングの完全なリストについては、PasswordEncoderFactories (Javadoc) の Javadoc を参照してください。

BCryptPasswordEncoder

BCryptPasswordEncoder 実装は、広くサポートされている bcrypt [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。パスワードクラッキングに対する耐性を高めるために、bcrypt は意図的に低速になっています。他の適応型一方向関数と同様に、システムでパスワードを検証するのに約 1 秒かかるように調整する必要があります。BCryptPasswordEncoder (Javadoc) の Javadoc に記載されているように、BCryptPasswordEncoder のデフォルトの実装は強度 10 を使用します。パスワードの確認に約 1 秒かかるように、独自のシステムで強度パラメーターを調整してテストすることをお勧めします。

BCryptPasswordEncoder
  • Java

  • Kotlin

// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Argon2PasswordEncoder

Argon2PasswordEncoder 実装は、Argon2 [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。Argon2 はパスワードハッシュコンペティション [Wikipedia] (英語) の勝者です。カスタムハードウェアでのパスワードクラッキングを無効にするために、Argon2 は、大量のメモリを必要とする意図的に低速なアルゴリズムです。他の適応型一方向性関数と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。Argon2PasswordEncoder の現在の実装には、BouncyCastle が必要です。

Argon2PasswordEncoder
  • Java

  • Kotlin

// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder 実装は、PBKDF2 [Wikipedia] アルゴリズムを使用してパスワードをハッシュします。パスワードクラッキングを打ち負かすには、PBKDF2 は意図的に遅いアルゴリズムです。他の適応型一方向性関数と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。このアルゴリズムは、FIPS 認定が必要な場合に適しています。

Pbkdf2PasswordEncoder
  • Java

  • Kotlin

// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

SCryptPasswordEncoder

SCryptPasswordEncoder 実装は、scrypt [Wikipedia] (英語) アルゴリズムを使用してパスワードをハッシュします。カスタムハードウェアでのパスワードクラッキングを無効にするために、scrypt は、大量のメモリを必要とする意図的に低速なアルゴリズムです。他の適応型一方向性関数と同様に、システムのパスワードを確認するのに約 1 秒かかるように調整する必要があります。

SCryptPasswordEncoder
  • Java

  • Kotlin

// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

その他の PasswordEncoder

下位互換性のために完全に存在する他の PasswordEncoder 実装が多数あります。これらはすべて非推奨であり、安全であるとは見なされなくなったことを示しています。ただし、既存のレガシーシステムを移行することは困難であるため、削除する予定はありません。

パスワード保存設定

Spring Security はデフォルトで DelegatingPasswordEncoder を使用します。ただし、PasswordEncoder を Spring Bean として公開することにより、これをカスタマイズできます。

Spring Security 4.2.x から移行する場合は、NoOpPasswordEncoder Bean を公開することにより、以前の動作に戻すことができます。

NoOpPasswordEncoder に戻すことは安全とは見なされません。代わりに、DelegatingPasswordEncoder の使用に移行して、安全なパスワードエンコーディングをサポートする必要があります。

NoOpPasswordEncoder
  • Java

  • XML

  • Kotlin

@Bean
public static NoOpPasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}
<b:bean id="passwordEncoder"
        class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@Bean
fun passwordEncoder(): PasswordEncoder {
    return NoOpPasswordEncoder.getInstance();
}

XML 構成では、NoOpPasswordEncoder Bean 名が passwordEncoder であることが必要です。

パスワード設定の変更

ユーザーがパスワードを指定できるほとんどのアプリケーションには、そのパスワードを更新する機能も必要です。

パスワードを変更するためのよく知られた URL (英語) は、パスワードマネージャーが特定のアプリケーションのパスワード更新エンドポイントを検出できるメカニズムを示します。

この検出エンドポイントを提供するように Spring Security を構成できます。例: アプリケーションのパスワード変更エンドポイントが /change-password の場合、次のように Spring Security を構成できます。

デフォルトのパスワード変更エンドポイント
  • Java

  • XML

  • Kotlin

http
    .passwordManagement(Customizer.withDefaults())
<sec:password-management/>
http {
    passwordManagement { }
}

次に、パスワードマネージャーが /.well-known/change-password に移動すると、Spring Security はエンドポイント /change-password をリダイレクトします。

または、エンドポイントが /change-password 以外の場合は、次のように指定することもできます。

パスワードエンドポイントの変更
  • Java

  • XML

  • Kotlin

http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )
<sec:password-management change-password-page="/update-password"/>
http {
    passwordManagement {
        changePasswordPage = "/update-password"
    }
}

上記の構成では、パスワードマネージャーが /.well-known/change-password に移動すると、Spring Security は /update-password にリダイレクトされます。

侵害されたパスワードのチェック

パスワードが侵害されていないかどうかを確認する必要があるシナリオがいくつかあります。たとえば、機密データを扱うアプリケーションを作成している場合、信頼性を主張するためにユーザーのパスワードをチェックする必要があることがよくあります。これらのチェックの 1 つは、通常、データ侵害 [Wikipedia] (英語) でパスワードが見つかったためにパスワードが侵害されているかどうかを確認することです。

CompromisedPasswordChecker API を自分で使用することも、DaoAuthenticationProvider は Spring Security 認証メカニズムを介してを使用している場合は CompromisedPasswordChecker Bean を提供することもできます。これは、Spring Security 構成によって自動的に取得されます。

そうすることで、弱いパスワード(たとえば 123456)を使用してフォームログイン経由で認証しようとすると、401 を受け取るか、/login?error ページにリダイレクトされます(ユーザーエージェントによって異なります)。ただし、その場合、401 またはリダイレクトだけではそれほど役に立ちません。ユーザーが正しいパスワードを入力したにもかかわらずログインできないため、混乱が生じます。このような場合、AuthenticationFailureHandler 経由で CompromisedPasswordException を処理して、ユーザーエージェントを /reset-password にリダイレクトするなど、必要なロジックを実行できます。例:

CompromisedPasswordChecker の使用
  • Java

  • Kotlin

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(authorize -> authorize
            .anyRequest().authenticated()
        )
        .formLogin((login) -> login
            .failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
        );
    return http.build();
}

@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
    return new HaveIBeenPwnedRestApiPasswordChecker();
}

static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
            "/login?error");

    private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException exception) throws IOException, ServletException {
        if (exception instanceof CompromisedPasswordException) {
            this.redirectStrategy.sendRedirect(request, response, "/reset-password");
            return;
        }
        this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
    }

}
@Bean
open fun filterChain(http:HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            authorize(anyRequest, authenticated)
        }
        formLogin {
            failureHandler = CompromisedPasswordAuthenticationFailureHandler()
        }
    }
    return http.build()
}

@Bean
open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
    return HaveIBeenPwnedRestApiPasswordChecker()
}

class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
    private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
    private val redirectStrategy = DefaultRedirectStrategy()

    override fun onAuthenticationFailure(
        request: HttpServletRequest,
        response: HttpServletResponse,
        exception: AuthenticationException
    ) {
        if (exception is CompromisedPasswordException) {
            redirectStrategy.sendRedirect(request, response, "/reset-password")
            return
        }
        defaultFailureHandler.onAuthenticationFailure(request, response, exception)
    }
}