Skip to content

ユースケース — bcrypt パスワードの移行 ​

本ライブラリが検証するパスワードハッシュは、argon2id の PHC 形式($argon2id$v=19$m=...,t=...,p=...$<salt>$<hash>)です。ほかのシステムから引き継いだ users テーブルには、たいてい bcrypt のハッシュが入っています。PrimaryPassword.LegacySchemes を指定すると、そのハッシュをそのまま検証でき、所有者が最初にサインインしたときに OP がハッシュを argon2id に変換します。一括リセットも、ハッシュ用の追加カラムも、移行ジョブも要りません。ユーザがログインするたびに、テーブルが自分で切り替わっていきます。

既存ユーザストアの投影の方法で、既存の users テーブルを OP の背後に置くときに使います。パスワードがすべて本ライブラリで作られたものなら、移行するものはありません。

ソース: examples/53-production-sql/users.go は、bcrypt ハッシュを持つ app_users テーブルをログインフローの背後に置き、所有者の次のログインで各ハッシュを書き換えます。

scheme を宣言する ​

go
op.WithLoginFlow(op.LoginFlow{Primary: op.PrimaryPassword{
  Store:         users,
  LegacySchemes: []op.PasswordHashScheme{op.BcryptHashScheme()},
}})

op.BcryptHashScheme() は、$2a$、$2b$、$2y$ で始まるハッシュを認識します。保存されたハッシュが argon2id でない場合、OP は LegacySchemes の scheme に宣言順で形式を認識できるか尋ね、最初に認識した scheme が検証します。どの scheme も認識しないハッシュは、誤ったパスワードと同じ扱いです。最初に見つかった 1 件は、プロセスごとに 1 回だけ WARN ログに記録されます。ログにユーザもハッシュも含まれず、LegacySchemes の設定を促す内容です。リストに nil の要素があると、op.New は失敗します。フィールドを nil または空にすると、argon2id だけを受け付けます。

op.PasswordHashScheme は 2 つのメソッドを持つ interface です。Recognizes(encoded []byte) bool は保存された形式から scheme を選び、Verify(ctx, encoded, password) (bool, error) は一致を報告します。Verify が error を返すのはバックエンドの障害のときだけで、不一致では返しません。別の形式の scheme を足すときも、bcrypt と同じ形で書きます。

ログインに成功したとき ​

  1. ReadPasswordHash が、保存されているハッシュをそのまま返します。
  2. scheme がパスワードを検証します。
  3. 一致したら、OP がパスワードを argon2id でエンコードし、UpdatePasswordHash に渡します。
  4. 次のログインでは argon2id のハッシュが見つかり、OP が直接検証します。

手順 3 には、PrimaryPassword の背後にあるストアがオプションの store.UserPasswordHashUpdater を実装している必要があります。

go
type UserPasswordHashUpdater interface {
  UpdatePasswordHash(ctx context.Context, subject string, encoded []byte) error
}

UpdatePasswordHash は subject のハッシュを置き換え、未知の subject には store.ErrNotFound を返さなければなりません。テーブルのカラムは、bcrypt ハッシュの 60 文字より長い新しい値を保持できる必要があります。example 53 は、自前の users テーブルへの UPDATE 1 本で実装しています。ReadPasswordHash は保存された値を形式にかかわらずそのまま返し、OP が $2a$ のハッシュは scheme に、$argon2id$ のハッシュは自前の検証器に振り分けます。

書き換えの成否が、ユーザが入れるかどうかを決めることはありません。書き込みに失敗するとログに記録しますが、ログインは成功し、書き換えは次のログインで再試行されます。

更新できないストア ​

updater がないと、ログインはできますが、レガシーのハッシュはそのまま残ります。何も変換されず、ログインのたびに bcrypt のコストを払い続けます。ストアが interface を実装したうえで store.ErrPasswordHashUpdateUnsupported を返すこともできます。この場合も OP はレガシーのハッシュを残し、ログを 1 回出力し、ユーザはこれまでどおりサインインできます。

同梱アダプタの挙動は次のとおりです。

ストアUpdatePasswordHash
op/storeadapter/sqloidc_users の行を更新
op/storeadapter/dynamodbユーザの item を更新
op/storeadapter/inmemメモリ上で更新
op/storeadapter/compositeユーザストアを持たない。永続バックエンド自身の UserPasswords() を渡す

composite アダプタはユーザストアを持たないので、users をほかのデータと分けるときは、永続バックエンドの UserPasswords()(updater を実装しています)を PrimaryPassword.Store に直接渡します。examples/17-spa-composite-store がこの形です。

ほかのログイン経路も argon2id を受け付ける必要があります

UpdatePasswordHash がユーザのハッシュを書き換えたあとは、そのユーザのパスワードを bcrypt で直接検証しているアプリケーション内のほかの経路(従来のログインフォーム、パスワード変更画面、パスワードを受け取る API など)が、そのユーザについて失敗するようになります。保存されているハッシュの接頭辞で検証方法を選び、argon2id には op.VerifyPassword を使ってください。

go
func checkPassword(hash []byte, password string) bool {
  switch {
  case bytes.HasPrefix(hash, []byte("$argon2id$")):
    return op.VerifyPassword(hash, password)
  case bytes.HasPrefix(hash, []byte("$2")):
    return bcrypt.CompareHashAndPassword(hash, []byte(password)) == nil
  }
  return false
}

すべての経路が argon2id を受け付けるようになるまで UserPasswordHashUpdater を実装しない、という選び方もあります。その間ハッシュは bcrypt のままで、ほかには何も変わりません。

変わらないこと ​

  • ロックアウト: レガシーのハッシュも、argon2id と同じ認証要素横断のロックアウトカウンタを通ります。ロックされた subject は、誤ったパスワードと同じ応答で拒否され、bcrypt の検証が総当たり対策を迂回することはありません。
  • 未知のユーザ: 未知のユーザを誤ったパスワードと区別できなくするダミー検証は、レガシーのハッシュにも適用されます。
  • claim: /userinfo と ID トークンの claim の読み元は変わりません。PrimaryPassword.Store に渡したストアを使う場合、claim はパスワードを検証するのと同じテーブルから読みます。

あらかじめ断っておく制限 ​

72 byte の上限: bcrypt はパスワードの先頭 72 byte しか使いません。op.BcryptHashScheme() は、72 byte を超えるパスワードに対して、切り詰めた先頭部分を比較せず不一致を報告します。そのため、長い文字列が偶然に一致することはありません。以前のシステムが長いパスワードを切り詰めてからハッシュ化していた場合、そのユーザはサインインできず、変換もできません。パスワードのリセットが必要です。

コストの上限: 保存されている bcrypt ハッシュの cost が 15 を超える場合、bcrypt を実行せずに拒否し、パスワードの不一致として扱います。そのユーザにはパスワードのリセットが必要です。

所要時間: レガシーのハッシュの検証にかかる時間は、argon2id とは異なります。ハッシュが書き換えられたユーザについては、この差はなくなります。

戻ってこないユーザ: サインインしないユーザは、bcrypt のハッシュを持ち続けます。残っている行数は、自分のテーブルへのクエリ(カラムの先頭が $2)で数えられます。数が十分に減ったら、残ったハッシュを廃止してリセットを求めるかどうかを決めてください。その時期について、ライブラリは判断を持ちません。

次へ ​