「全員にパスワード再設定して」が無理だった話
Firebase Auth から bcrypt の自前認証へ移行しようとした時、最初にぶつかった壁がこれでした。
「既存ユーザーのパスワード、どうする?」
パスワードはFirebase側に保存されていて、自分のDBには無い。しかも当然、平文では取り出せない。Firebase独自のハッシュ形式と bcrypt は互換性がない。「じゃあ全員にパスワード再設定してもらうか…」と一瞬考えるんですが、これが無理筋なんです。
- 大量のユーザーに「パスワード再設定してください」と一斉メール → 多くの人は無視する
- 強制ログアウト&再設定 → 離脱が一気に発生する
- 「なんで急に?」とユーザーから問い合わせが殺到
- 運営の信頼を損なう
悩んでいたら、調べる中で 「lazy rehash」 というパターンに行き着きました。「は?lazy rehash?なんやねん」と心の中でツッコみつつ、内容を読んだら「それだ!」と膝を打ちました。
結論を先に言うとこういうことです。
- lazy rehash=アクセスのタイミングで、古いハッシュを新しい形式に変換していく設計パターン
- パスワード文脈なら:ログイン時に旧認証で本人確認 → 入力された平文を新ハッシュ化 → DB保存
- ユーザー目線では何も変わらない(普通にログインするだけ)
- バックエンドだけが裏でじわじわ移行していく
本記事では、「ユーザーに乱暴な再設定を強制せずに、認証システムをこっそり段階移行する」方法として、lazy rehash パターンを実体験ベースで解説します。Firebase脱却を考えている人、bcryptに移行したい人、saltRoundsを上げたい人、認証強化を悩んでいる人に刺さる内容です。
そもそも:なぜパスワード移行はこんなに大変なのか
本題のlazy rehashに入る前に、なぜパスワード移行が独特の難しさを持つのかを整理しておきましょう。
パスワードは「平文では絶対に保存されていない」
セキュリティ上、まともなサービスは パスワードを必ずハッシュ化して保存しています。Firebase でも、AWS Cognito でも、Auth0 でも、自前認証でも基本的に同じ。これは bcryptでハッシュ化する|初心者エンジニア向けセキュリティ実装入門 でも詳しく解説されている、認証の絶対前提です。
つまり、移行元のサービスから「全ユーザーのパスワード平文を一括ダウンロード」みたいなことは原理的にできない。これが移行の難しさの根本です。
ハッシュ形式に互換性がない
「じゃあハッシュ値だけコピーすればいいじゃん」と思うかもしれません。でも、各サービスが採用しているハッシュアルゴリズムは違うので、ハッシュ値同士に互換性がない。
- Firebase:内部的にscryptベース(厳密にはFirebase独自仕様)
- bcrypt:自前認証でよく使われる業界標準の一つ
- Argon2:bcryptの後継として推奨される最新規格
- SHA1 / MD5:古いシステムに残っている非推奨アルゴリズム
これらは 同じパスワードからでも全く違うハッシュ値を生成するので、変換不可能。ハッシュ値を直接コピーしても動きません。
「全員パスワード再設定」がユーザー離脱の温床
残された選択肢は「強制パスワード再設定」ですが、これがビジネス的に最悪。経験則として、強制リセットしたユーザーの3〜5割は二度とログインしません。「面倒だな…」「このサービスもう使ってないし」のコンボで休眠化していきます。
つまり、「ユーザーに気付かせずに、舞台裏でこっそり移行する」必要があるんです。そこで登場するのが lazy rehash。
改めて:lazy rehash とは何者か
lazy rehash(レイジー・リハッシュ)は、ハッシュ値の更新を「必要になったタイミングで少しずつ行う」設計パターンです。
ひと言定義はこれ。
👉 「アクセス(ログイン)のタイミングで、古い形式のハッシュを新しい形式にリハッシュしていくパターン」
「lazy(遅延)」とは、必要になるまでやらない、という意味。プログラミング用語の遅延評価(lazy evaluation)と同じ思想で、「全件一気に処理する代わりに、本当に必要になったタイミングで少しずつ処理する」戦略を指します。
教科書的な動作
[ユーザーがログイン]
↓
[アプリ] そのユーザーのハッシュ形式を確認
↓
├─ 旧形式の場合 → 旧方式で検証 → 成功なら新形式でハッシュ化&DB更新
└─ 新形式の場合 → 新方式で検証 → そのまま完了
↓
レスポンス返却(ユーザーは何が起きたか知らない)ポイントは、ユーザーには平文パスワードが入力された瞬間(ログイン時)にしか、新形式でハッシュ化するチャンスがないということ。だからこそ「ログインのタイミング」を移行のトリガーにします。
パスワード移行で起きていた “lazy rehash” の正体
もう少し具体的に、Firebase → bcrypt 移行の文脈で見ていきましょう。
Before:Firebase認証のみ
[ユーザー] ログイン要求
↓
[アプリ] Firebase Auth に問い合わせ
↓
[Firebase] 認証成功 → ユーザー情報返却
↓
レスポンス返却
→ パスワードハッシュは Firebase が握っている
→ 自前のDBは認証情報を持っていないAfter:lazy rehash で段階移行
[ユーザー] ログイン要求
↓
[アプリ] DBを確認
↓
├─ DBにbcryptハッシュあり → bcryptで検証 → 完了(移行済み)
│
└─ DBにハッシュなし(未移行ユーザー)
↓
[Firebase] 認証成功
↓
★lazy rehash★
・入力された平文パスワードをbcryptでハッシュ化
・DBに保存
↓
次回以降はDBで完結
→ ユーザーには何も特別なことが起きてないように見える
→ 自然にログインしただけで、裏でひっそり移行が進むこれが lazy rehash パターンの全貌。「ログインの瞬間だけが、平文パスワードを取得できる唯一のチャンス」という制約を逆手に取った、エレガントな設計です。
Next.jsでの実装例
具体的なコードを見ていきましょう。Next.js (App Router) + AWS SDK v3 (DynamoDB Documentクライアント) + bcrypt の構成です。
基本のlazy rehash実装
// app/api/login/route.ts
import { signInWithEmailAndPassword } from 'firebase/auth'
import bcrypt from 'bcrypt'
import { auth } from '@/lib/firebase'
import { docClient } from '@/lib/dynamodb'
import { GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb'
const SALT_ROUNDS = 10
export async function POST(req: Request) {
const { email, password } = await req.json()
// Step 1: DBにbcryptハッシュがあるか確認
const result = await docClient.send(new GetCommand({
TableName: 'Users',
Key: { email },
}))
const localUser = result.Item
// Step 2: 既に移行済みならbcryptで検証して終わり
if (localUser?.passwordHash) {
const isValid = await bcrypt.compare(password, localUser.passwordHash)
if (!isValid) {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
return Response.json({ user: localUser })
}
// Step 3: 未移行ユーザー → Firebaseで認証
let userCredential
try {
userCredential = await signInWithEmailAndPassword(auth, email, password)
} catch {
return Response.json({ error: 'Invalid credentials' }, { status: 401 })
}
// Step 4: ★ここがlazy rehash★
// Firebase認証成功 → 平文パスワードをbcrypt化 → DB保存
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS)
const newUser = {
email,
passwordHash,
firebaseUid: userCredential.user.uid,
migratedAt: new Date().toISOString(),
}
await docClient.send(new PutCommand({
TableName: 'Users',
Item: newUser,
}))
return Response.json({ user: newUser })
}このコードのポイントは、「ユーザーが普通にログインするだけで、自然と新しい認証システムにマイグレートされていく」こと。一斉メンテナンス不要、ユーザーへの通知不要、データ取得バッチ不要。気づけば全員のデータがDB側に揃っている美しい設計です。
処理の流れまとめ
初めてログインするユーザー(未移行):
DBに無い → Firebaseで認証 → bcrypt化してDB保存
→ 移行完了
2回目以降のログイン:
DBにあり → bcryptで検証 → そのまま認証完了
→ Firebaseは呼ばれない
新規登録ユーザー:
最初からDBに直接bcryptハッシュで保存
→ Firebaseは介在しないこれは Firebase Auth + DynamoDB のDB分断地獄から抜け出した話 で解説した write-through パターンと兄弟関係にあります。両方とも「アクセスを契機に段階移行する」発想で、組み合わせて使えます。
lazy rehash が活きる他のシーン
lazy rehashはFirebase→bcrypt移行に限らず、「古い形式のデータを新しい形式に少しずつアップグレードする」あらゆる場面で使えます。代表的なケースを紹介。
シーン1:bcryptのcost値アップグレード
bcrypt の saltRounds(cost)は、計算負荷を上げてセキュリティを高めるパラメータ。マシンの性能向上に合わせて、定期的に上げる必要があります。例えば、5年前は10で十分だったが、今は12が推奨レベル。
const TARGET_SALT_ROUNDS = 12 // 現在の推奨値
async function authenticate(email: string, password: string) {
const result = await docClient.send(new GetCommand({
TableName: 'Users',
Key: { email },
}))
const user = result.Item
if (!user) return null
const isValid = await bcrypt.compare(password, user.passwordHash)
if (!isValid) return null
// ★lazy rehash★ 古いcostだったら新しいcostでrehash
const currentCost = bcrypt.getRounds(user.passwordHash)
if (currentCost < TARGET_SALT_ROUNDS) {
const newHash = await bcrypt.hash(password, TARGET_SALT_ROUNDS)
await docClient.send(new PutCommand({
TableName: 'Users',
Item: { ...user, passwordHash: newHash },
}))
}
return user
}これで、ログインしたユーザーから順に最新のcostにrehashされていきます。セキュリティを段階的に底上げできるのがポイント。
シーン2:SHA1 / MD5 から bcrypt へ
古いシステムで SHA1 や MD5 でハッシュ化されたパスワードがある場合、ログイン時に bcrypt にrehash していくパターンも頻出。これも基本構造は同じです。
import crypto from 'crypto'
async function authenticate(email: string, password: string) {
const user = await getUserByEmail(email)
if (!user) return null
// ハッシュ形式を判定
if (user.passwordHash.startsWith('$2')) {
// bcrypt形式(移行済み)
return (await bcrypt.compare(password, user.passwordHash)) ? user : null
} else {
// 旧SHA1形式 → ★lazy rehash★
const sha1Hash = crypto.createHash('sha1').update(password).digest('hex')
if (sha1Hash !== user.passwordHash) return null
// 検証成功なら、bcrypt化してDB更新
const newHash = await bcrypt.hash(password, 10)
await updateUserHash(email, newHash)
return user
}
}シーン3:暗号化方式・データフォーマットのバージョン更新
パスワードに限らず、暗号化されたフィールド全般、あるいはバージョン管理されたデータ全般でlazy rehashの発想は応用できます。
- クレジットカード番号の暗号化方式更新(古いキー → 新しいキー)
- JSONスキーマのバージョン v1 → v2 移行(読み込み時に変換して保存)
- シリアライズ形式の変更(JSON → MessagePack など)
共通する発想は、「全件一気にマイグレーションするコストとリスク」を避けて「アクセス契機で少しずつ」更新するところ。エンタープライズシステムの段階移行戦略の鉄板パターンです。
実装で押さえるべきポイント
lazy rehash は強力なパターンですが、雑に実装するとセキュリティ事故の温床になります。実装時の注意点を整理しておきましょう。
ポイント1:平文パスワードの取り扱い
ログイン時に 平文パスワードがメモリ上に存在するのは、lazy rehash の前提です。だからこそ、扱いを慎重にする必要があります。
- ログに絶対出さない:意図しない出力でパスワードが漏れる事故を防ぐ
- 変数のスコープを最小化:必要な範囲だけで使い、関数を抜けたら参照を切る
- HTTPS必須:通信経路の暗号化は当たり前
- 監視ツールでマスキング:Sentry や Datadog でリクエストボディを送る場合、パスワードフィールドはマスク必須
セキュリティ全般の基本は DDoS攻撃とは?開発者が知るべきセキュリティの基本 あたりも押さえておくと、認証周りのリスク感が研ぎ澄まされます。
ポイント2:DB更新の失敗ハンドリング
「Firebase認証は成功したけど、DB書き込みは失敗」というケースの考慮が必要。基本方針は、「DB更新失敗してもログイン自体は成功させる」です。次回ログイン時にまた挑戦すればいいので、ユーザー体験を優先します。
// DB書き込み失敗してもログインは成功させる
try {
await docClient.send(new PutCommand({
TableName: 'Users',
Item: newUser,
}))
} catch (error) {
// ログには記録するが、ユーザーには影響を与えない
console.error('lazy rehash失敗:', error)
// 次回ログイン時に再試行される
}
return Response.json({ user: { email, ...userInfo } })ポイント3:移行進捗の可視化
lazy rehashの欠点は、「いつ全員の移行が完了するか分からない」こと。だからこそ、進捗を可視化しておくことが重要。migratedAt や migration_version といったフィールドを保存しておくと、後で集計できます。
// 移行済みユーザー数を集計(DynamoDB Scan)
const migrated = await docClient.send(new ScanCommand({
TableName: 'Users',
FilterExpression: 'attribute_exists(passwordHash)',
Select: 'COUNT',
}))
const total = await docClient.send(new ScanCommand({
TableName: 'Users',
Select: 'COUNT',
}))
console.log(`移行率: ${migrated.Count} / ${total.Count}`)ポイント4:期限を切った最終移行戦略
lazy rehashの致命的弱点は 「ログインしないユーザーは永遠に移行されない」こと。1年放置していたユーザーは、いつまで経っても旧形式のままです。
そのため、最終的にはどこかで「強制移行」の決断が必要。一般的な戦略は以下の3段階。
- Phase 1(数ヶ月):lazy rehashで自然な移行を進める
- Phase 2(数週間):移行率が一定(例:90%)に達したら、未移行ユーザーへ事前通知メール
- Phase 3(最終日):未移行ユーザーは強制パスワード再設定 or アカウント休眠化
こうすることで、離脱リスクを最小化しつつ、最終的に旧システムを完全に切り離すことができます。
lazy rehash の限界とトレードオフ
万能ではないので、デメリットや限界もちゃんと把握しておきましょう。
メリット
- ユーザー体験を維持:パスワード再設定の強制が不要
- 運用コスト低:一斉メンテナンスやバッチ処理が不要
- 段階的・低リスク:問題が見つかってもダメージが局所的
- セキュリティの底上げ:cost値や暗号方式を継続的に強化できる
デメリット
- 完了時期が読めない:休眠ユーザーは永遠に移行されない
- 並行運用のコスト:旧システム(Firebase等)を当面動かし続ける必要がある
- コードが複雑化:認証ロジックに分岐が増える
- 最終的な強制移行は避けられない:完全廃止には決断の日が来る
他の選択肢との比較
| 移行戦略 | ユーザー影響 | 実装複雑度 | 移行完了の予測 |
|---|---|---|---|
| 全員強制リセット | 大(離脱リスク) | 低 | 明確 |
| lazy rehash(本記事) | なし | 中 | 不明確 |
| マネージドサービス利用 | なし | 低 | サービス依存 |
| 並行稼働+切り替え | 少 | 高 | 明確 |
「マネージドサービスでいいや」と判断する場合は、AWS Cognitoとは?初心者向け認証サービス完全ガイド も併せて読んで、自前認証 vs マネージドの判断軸を持っておきましょう。
似た設計パターンとの関係
lazy rehash は単独のパターンというより、「アクセス契機で段階移行する」設計パターン群の一員です。兄弟関係にあるパターンを把握しておくと、システム設計の引き出しが広がります。
| パターン名 | 動き | 主な用途 |
|---|---|---|
| lazy rehash | アクセス時に古いハッシュを新しい形式に変換 | パスワード移行、cost更新 |
| write-through | 書き込み時にキャッシュとDBに同時書き込み | 外部認証から自前DBへの移行 |
| cache-aside | キャッシュにないとき手動でDBから読む | 記事一覧、商品情報のキャッシュ |
| read-through | キャッシュにないとき自動でDBから読む | 透過的キャッシュ層 |
共通する発想は、「すべてを一気にやるのではなく、必要になったタイミングで少しずつ」。クラウドネイティブな世界では、この発想が運用の安定性に直結します。Redis入門 あたりで触れられているキャッシュ戦略を理解しておくと、これらのパターンが地続きで見えてきます。
まとめ:「アクセス契機で少しずつ」が認証移行の現実解
本記事のポイントを整理します。
- lazy rehash=アクセス時に古いハッシュを新しい形式に変換していく設計パターン
- パスワードは平文取り出し不可なので、ログイン時の平文入力が唯一のチャンス
- 「全員パスワード再設定」は離脱の温床、lazy rehash でこっそり移行
- Firebase → bcrypt 移行に限らず、cost値更新・SHA1→bcrypt・暗号方式更新など多用途
- 実装で押さえるべきは4点:平文取扱い、DB失敗ハンドリング、進捗可視化、最終移行戦略
- 限界もある:ログインしないユーザーは永遠に移行されない、最終的な強制移行が必要
- write-through との兄弟パターン:「アクセス契機の段階移行」という発想が共通
「lazy rehash」って、最初は「なんやねん」だったこの用語も、認証システムの段階移行の現実解として腹落ちすると、急に親近感が湧きます。「ユーザーに気付かせずに、舞台裏でこっそり移行する」という発想は、エンタープライズシステムのアップグレードでも応用できる、汎用性の高いパターン。
もしあなたも今、認証システムの移行で「全員にパスワード再設定してもらうのはちょっと…」と悩んでいるなら、lazy rehash パターンを実装してみてください。ユーザーには何も告げず、こっそり移行を進めるという、エンジニアの粋な設計です。
そして、こうした設計パターンに名前を覚えると、技術用語の「なんやねん」は確実に減っていきます。エンジニアが読めないIT用語25選 でも触れているように、用語を知ることはエンジニアの語彙力。次のリファクタリングで「これlazy rehashで行けるね」と即答できる自分になっていきましょう。
認証・段階移行の知識をさらに深める関連記事
lazy rehash パターンを理解したら、認証システム全体の設計力や、関連する段階移行パターンの引き出しを増やしていきましょう。実装の幅が広がる関連記事を厳選しました。
認証・段階移行の引き出しを増やす
- Firebase Auth + DynamoDB のDB分断地獄から抜け出した話|write-throughで段階移行する設計パターン – 本記事と兄弟関係のwrite-throughパターン解説。「アクセス契機の段階移行」を別角度から押さえられます
- bcryptでハッシュ化する|初心者エンジニア向けセキュリティ実装入門 – 本記事で何度も登場するbcryptの基礎を徹底解説。saltRoundsの選び方、検証実装の詳細まで網羅
- AWS Cognitoとは?初心者向け認証サービス完全ガイド – 自前認証 vs マネージドサービスの判断軸。lazy rehashで自前移行する vs 一気にCognitoに移すかの選択基準が見えてきます
設計パターン・キャッシュ戦略を強化
- Redis入門!爆速データ処理の秘密と活用術をやさしく解説 – lazy rehashと兄弟関係のキャッシュ戦略パターンが多数登場。インメモリDBの仕組みからTTL設定まで実用的に解説
- 「全部POSTでよくない?」がモヤる人へ|HTTPメソッド使い分けの正論と現場のリアル – 認証APIの設計と密接な関連。POST/GETの使い分けがlazy rehashの実装にも効いてきます
セキュリティ・用語の引き出し
- DDoS攻撃とは?開発者が知るべきセキュリティの基本 – 認証システムを設計する上で押さえておくべきセキュリティの基礎。攻撃ベクトルを理解すると守りが固まります
- 【初見殺し】エンジニアが読めないIT用語25選|nginx→ンギンクス?の悲劇を防ぐ – lazy rehashみたいなマニアック用語に強くなる土台。エンジニアの語彙力を底上げする読み物
