パスワードを確認する
CoddyのNode.jsジャーニー「認証とセキュリティ」セクションの一部。レッスン 6/24。
ログインでは、保存されたソルトと元のキーの長さを使って、入力されたパスワードからキーを導出します。正しいパスワードであっても、新しいソルトを使うと異なるキーになります。元のパスワードがストレージから復号されることはありません。
// 招待コードはsalt.keyHexとして保存されていた
const [salt, keyHex] = savedInvite.split('.');
const expected = Buffer.from(keyHex, 'hex');
const attempted = await scryptAsync(inviteCode, salt, 32);長さが一致することを確認した後、結果のバイト列をtimingSafeEqualで比較します。これにより、内容に依存する早期終了によってタイミングから情報が漏れるのを防ぎます。Buffer の===は同一性をチェックし、通常の文字列比較には一定時間で処理される保証がありません。パスワード関数では、パスワードレコードのコロン区切り文字と64バイトのキーを使用します。追加するPOST /loginルートは、この結果を使って200または401を返します。
チャレンジ
簡単このステップでは、Never Store Passwords のプロジェクトファイル(passwords.js を含む)ではなく、コースで用意されたプライベートノート API プロジェクトを開きます。以前の作業は、それぞれのレッスンに残っています。passwords.js を編集する前に、提供されたファイルを読んでください。このコピーには、パスワードのハッシュ化ヘルパーと、用意されたデバッグ用ルートが含まれています。
サインアップでは、ソルト付きハッシュを保存するようになりました。サインアップで保存した値とパスワードを照合するログイン機能を追加しましょう。
passwords.jsに、async関数verifyPassword(password, stored)を追加してエクスポートしてください。storedは、サインアップで保存された値で、たとえばf2bb...f065:c3f7...dbd2のようなものです。passwordがその値を生成したパスワードならtrueを返し、そうでなければfalseを返してください。- コロンより前の部分からソルトを取り出します。
hashPasswordと同じ方法で、そのソルトを使ってpasswordをハッシュ化し、64バイトのキーを生成します。- コロンより後ろにある保存済みのキーを、16進数からデコードします。2つのキーのバイト長が異なる場合は、「キーのバイト列の比較」と同様に、
timingSafeEqualを呼び出さずにfalseを返してください。それ以外の場合は、比較結果を返します。
server.jsで、hashPasswordと並べてverifyPasswordをインポートしてください。POST /loginを追加します。サインアップと同じ本文を受け取り、たとえば{"username": "ada", "password": "correct horse"}のようになります。- そのユーザー名のユーザーを検索します。
verifyPasswordは処理に時間がかかり、promise を返すため、完了を待ってください。 - ユーザーが存在し、パスワードが正しい場合は、ステータス200と
{"username": "ada"}を返します。 - それ以外の場合は、ステータス401と
{"error": "Wrong username or password"}を返します。401 Unauthorized は、リクエストで要求者が誰なのかを証明できなかったことを意味します。 - パスワードが間違っている場合も、サインアップしていないユーザー名の場合も、まったく同じ401を返してください。そうすれば、レスポンス本文から、どのユーザー名が登録されているかが誰にも分かりません。
- そのユーザー名のユーザーを検索します。
試してみましょう: 「実行」を押してください。POST /signup に {"username": "ada", "password": "correct horse"} を送信すると、ステータスは201です。同じ本文で POST /login を送信すると、ステータス200と {"username": "ada"} が返ります。{"username": "ada", "password": "wrong horse"} を使って POST /login を送信すると、ステータス401と {"error": "Wrong username or password"} が返ります。{"username": "nobody", "password": "correct horse"} を使って POST /login を送信すると、同じ401が返ります。
自分で試してみよう
import { randomBytes, scrypt } from 'node:crypto';
import { promisify } from 'node:util';
const scryptAsync = promisify(scrypt);
// パスワードをsignupが保存するテキストに変換します。
export async function hashPassword(password) {
const salt = randomBytes(16).toString('hex');
const key = await scryptAsync(password, salt, 64);
return `${salt}:${key.toString('hex')}`;
}
認証とセキュリティのすべてのレッスン
自分で練習してみよう: Expressプレイグラウンド