Menu
CoddyTech

パスワードを確認する

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を返します。

challenge icon

チャレンジ

簡単

このステップでは、Never Store Passwords のプロジェクトファイル(passwords.js を含む)ではなく、コースで用意されたプライベートノート API プロジェクトを開きます。以前の作業は、それぞれのレッスンに残っています。passwords.js を編集する前に、提供されたファイルを読んでください。このコピーには、パスワードのハッシュ化ヘルパーと、用意されたデバッグ用ルートが含まれています。

サインアップでは、ソルト付きハッシュを保存するようになりました。サインアップで保存した値とパスワードを照合するログイン機能を追加しましょう。

  1. passwords.js に、async 関数 verifyPassword(password, stored) を追加してエクスポートしてください。stored は、サインアップで保存された値で、たとえば f2bb...f065:c3f7...dbd2 のようなものです。password がその値を生成したパスワードなら true を返し、そうでなければ false を返してください。
    • コロンより前の部分からソルトを取り出します。
    • hashPassword と同じ方法で、そのソルトを使って password をハッシュ化し、64バイトのキーを生成します。
    • コロンより後ろにある保存済みのキーを、16進数からデコードします。2つのキーのバイト長が異なる場合は、「キーのバイト列の比較」と同様に、timingSafeEqual を呼び出さずに false を返してください。それ以外の場合は、比較結果を返します。
  2. 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プレイグラウンド