認証は、アプリケーションの入り口に立つ用心棒のようなものです。ユーザーがサインアップしたりログインしたりするたびに、システムはその人物が自称通りの本人であるかどうかを判断しなければなりません。もし判断を誤れば、単にログイン失敗のデバッグで済む話ではありません。ユーザーの実際のデータが、そのまま外部へ流出してしまうリスクを負うことになります。これを適切に行うには、2つの信頼できるツールが必要です。保存されたパスワードを保護するためのBcryptと、ユーザーがアプリ内を移動する際の身元確認を行うためのJSON Web Tokenです。
なぜ平文での保存は失敗するのか
この記事からたった一つのルールだけを覚えて帰るとすれば、これにしてください。データベースにパスワードを平文(プレーンテキスト)で保存してはいけません。データベースがファイアウォールの背後にあるか、あるいはチームのエンジニア全員を信頼しているかは関係ありません。パスワードをそのままの形でテーブルに書き込むのは、玄関のマットの下に家の鍵を置いておくようなものです。設定ミスのあるAPI、流出したバックアップ、あるいはインジェクション攻撃などを通じて誰かがそのデータベースにアクセスした瞬間、すべての認証情報が即座に露呈します。
人々はパスワードを使い回すため、被害は拡大します。たった一度の侵害が、あなたのアプリケーションだけでなく、ユーザーのメール、銀行、SNSのアカウントまでも危険にさらす可能性があるのです。だからこそ、パスワードをハッシュ化するのです。ハッシュ化とは、パスワードを元のものとは似ても似つかない、バラバラな文字列に変換することです。このプロセスは一方向であり、不可逆的です。ハッシュに対して「数学的な酸」を注いでも、それを元のパスワードに戻すことはできません。
Bcryptによるパスワードのハッシュ化
Bcryptは、パスワードのために特別に構築されたハッシュ関数です。平文の文字列を受け取り、Blowfish暗号を通して、$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy のような結果を返します。その接頭辞(プレフィックス)によって、アルゴリズムとコスト係数がわかります。長い末尾の部分は、ソルトとハッシュそのものの組み合わせです。
ソルトとは、ハッシュ化が始まる前にパスワードに混ぜられるランダムなデータのことです。ソルトはユーザーごとに固有であるため、たとえ2人のユーザーが共に "password123" を選択したとしても、データベース上では全く異なるハッシュになります。この些細な違いが、レインボーテーブル(よく使われるパスワードのハッシュ値をあらかじめ計算して用意した辞書)を無効化します。攻撃者がすべての固有のソルトに対して、テーブル全体を再生成しなければならなくなるからです。
Bcryptは、意図的に低速に設計されています。現代のハードウェアは、SHA-256のような高速なアルゴリズムを使えば、1秒間に数十億ものハッシュを推測できます。しかし、Bcryptはあえて時間をかけます。コンピュータの性能が向上するにつれて引き上げることができる「コスト係数」によって制御され、ハッシュ化プロセスを何度も繰り返します。その遅さこそが、盗まれたデータベースに対して総当たり攻撃(ブルートフォース攻撃)を仕掛けようとする者への罰となります。ログイン時にわずか200ミリ秒余計に待たされる正規のユーザーは、それに気づかないでしょう。しかし、何百万回もの推測を試そうとする攻撃者は、間違いなくその影響を受けます。
JSON Web Tokenの仕組み
Bcryptが玄関口を管理する一方で、JWTは「廊下を通るための通行証」を管理します。JSON Web Tokenは、ユーザーがすでに認証済みであることを証明する、コンパクトでURLセーフな文字列です。サーバーが発行し、クライアントが持ち歩くデジタルIDカードだと考えてください。
JWTは、ドットで区切られた3つのセグメント(ヘッダー、ペイロード、署名)で構成されています。ヘッダーは、トークンの種類と署名アルゴリズムを指定します。ペイロードには、ユーザーID、ユーザー名、有効期限のタイムスタンプなど、ユーザーやトークン自体に関するクレーム(主張)が含まれます。署名は暗号学的な封印です。サーバーはヘッダーとペイロードをエンコードし、それを秘密鍵に通すことで署名を作成します。もし誰かがペイロードを改ざんすれば、署名が一致しなくなり、サーバーはそのトークンを即座に拒否します。
