一覧へ

複数ユーザー認証の実装

サインアップ、パスワードハッシュ化(bcrypt)、ロールベースのアクセス制御の実装方法をステップごとに学びます。

中級
|
12
|
検証済み (2026-07)
複数ユーザーサインアップbcryptパスワードハッシュ化認証の実装
進捗0/55 (0%)

多ユーザー認証の実装

このトピックを完了すると:

ユーザー登録中にパスワードを安全にハッシュ化し、bcryptの原理を理解し、ロールベースのアクセス制御を実装できるようになります。


パスワードを平文で保存することの危険性

javascript
// 絶対にこれを行わないでください
const users = [
  { username: 'alice', password: 'mySecret123' }
];

データベースが侵害された場合、すべてのユーザーのパスワードが公開されます。2012年のLinkedInハッキングと2013年のAdobeハッキングでは、平文(または弱いハッシュ)で保存された数億件のパスワードが公開されました。

パスワードを平文で保存しないでください。 ハッシュを使用して保存してください。


ハッシュとは?

ハッシュは一方向変換です。元のデータからハッシュを生成することは可能ですが、ハッシュから元のデータを復元することはできません。

text
"mySecret123" → ハッシュ化 → "$2b$10$X7kG..."  (可能)
"$2b$10$X7kG..." → ???  → "mySecret123"   (不可能)

ログイン時に、入力されたパスワードをハッシュ化し、保存されているハッシュと比較します。

text
ユーザー入力: "mySecret123"
保存されているハッシュ: "$2b$10$X7kG..."

"mySecret123" → ハッシュ化 → "$2b$10$X7kG..."  (一致 → 認証成功)
"wrongPass"   → ハッシュ化 → "$2b$10$Qm1Y..."  (不一致 → 認証失敗)

bcrypt — パスワードハッシュの標準

bash
npm install bcrypt

bcryptが、パスワードに対してSHA-256のような一般的なハッシュ関数よりも優れている理由:

  1. 遅く動作するように設計されている: ハッカーが1秒間に数十億のハッシュを試すのを防ぎます。
  2. 自動的にソルトを含める: 同じパスワードでも、毎回異なるハッシュを生成します。
  3. 調整可能なコスト: roundsの値で、速度とセキュリティのトレードオフを調整できます。
javascript
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 10;

// ハッシュ化(登録時)
const hash = await bcrypt.hash('mySecret123', SALT_ROUNDS);
console.log(hash);
// $2b$10$X7kG.fDlUqVnPq5RvWEYq.ZM3Y4Fs8qD1vSrKLhwEcFjnp2fW9Xm.

// 比較(ログイン時)
const isMatch = await bcrypt.compare('mySecret123', hash);
console.log(isMatch);  // true

const isWrong = await bcrypt.compare('wrongPass', hash);
console.log(isWrong);  // false

SALT_ROUNDS = 10は、2^10 = 1,024回の反復を意味します。より高い値はより安全ですが、処理が遅くなります。現在の推奨範囲は10〜12です。


登録の実装

javascript
const express = require('express');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());

const users = [];  // 実際のアプリケーションでは、データベースを使用します。

app.post('/register', async (req, res) => {
  const { username, password, displayName } = req.body;

  // 入力検証
  if (!username || !password) {
    return res.status(400).json({ error: 'Username and password required' });
  }
  if (password.length < 8) {
    return res.status(400).json({ error: 'Password must be at least 8 characters' });
  }

  // 重複の確認
  if (users.find(u => u.username === username)) {
    return res.status(409).json({ error: 'Username already exists' });
  }

  // パスワードのハッシュ化
  const hashedPassword = await bcrypt.hash(password, 10);

  // ユーザーの保存
  const newUser = {
    id: users.length + 1,
    username,
    password: hashedPassword,
    displayName: displayName || username,
    role: 'user',
    createdAt: new Date().toISOString()
  };
  users.push(newUser);

  res.status(201).json({
    message: 'Registration successful',
    user: { id: newUser.id, username: newUser.username }
  });
});

パスワード(ハッシュを含む)をレスポンスに含めないでください。


Passportとの統合

javascript
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

passport.use(new LocalStrategy(
  async (username, password, done) => {
    const user = users.find(u => u.username === username);
    if (!user) {
      return done(null, false, { message: 'User not found' });
    }

    // bcryptを使用して比較
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) {
      return done(null, false, { message: 'Wrong password' });
    }

    return done(null, user);
  }
));

passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => {
  const user = users.find(u => u.id === id);
  done(null, user);
});

前のトピックのStrategyからの唯一の違いは、user.password !== passwordの代わりにbcrypt.compare()を使用することです。


ロールベースのアクセス制御(RBAC)

javascript
function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.isAuthenticated()) {
      return res.status(401).json({ error: 'Login required' });
    }
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({ error: 'Insufficient permissions' });
    }
    next();
  };
}

// すべてのログインユーザー
app.get('/profile', requireRole('user', 'admin'), (req, res) => {
  res.json({ user: req.user.displayName });
});

// 管理者のみ
app.get('/admin/users', requireRole('admin'), (req, res) => {
  const safeUsers = users.map(u => ({
    id: u.id, username: u.username, role: u.role
  }));
  res.json(safeUsers);
});

// 管理者 - ロールの変更
app.patch('/admin/users/:id/role', requireRole('admin'), (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'User not found' });

  user.role = req.body.role;
  res.json({ message: `${user.username} role changed to ${user.role}` });
});

requireRole(...roles)は、高階関数(関数を返す関数)です。許可されるロールを引数として受け取り、ユーザーに必要なロールを持っていない場合に403を返すミドルウェアを作成します。


セキュリティチェックリスト

項目説明
パスワードハッシュbcrypt、SALT_ROUNDS 10+
最小文字数8文字以上(NISTの推奨)
ハッシュをレスポンスに含めないパスワードフィールド(ハッシュを含む)をAPIレスポンスに含めないでください
一般化されたエラーメッセージ「無効なユーザー名またはパスワード」(どちらが間違っているかを明示しない)
レート制限ログイン試行回数を制限する(ブルートフォース攻撃の防止)
HTTPSパスワードがネットワーク経由で平文で送信されないようにする

エラーメッセージを一般化する理由:ユーザー名が見つからないと伝えると、攻撃者はアカウントの存在を判断できます。これはユーザー列挙攻撃と呼ばれます。


実際のパターン — アカウントの削除

javascript
app.delete('/account', requireRole('user', 'admin'), async (req, res) => {
  const valid = await bcrypt.compare(req.body.password, req.user.password);
  if (!valid) {
    return res.status(401).json({ error: 'Password confirmation failed' });
  }

  const index = users.findIndex(u => u.id === req.user.id);
  users.splice(index, 1);

  req.logout((err) => {
    if (err) return res.status(500).json({ error: 'Logout failed' });
    res.json({ message: 'Account deleted' });
  });
});

アカウントの削除はリスクの高い操作であるため、パスワードの再確認が必要です。削除後、req.logout()でセッションをクリアしてください。


主要なポイント

概念概要
ハッシュ一方向変換。元のデータからハッシュを生成することは可能ですが、ハッシュから元のデータを復元することはできません
bcryptパスワード専用のハッシュ。遅く動作し、ソルトを含める
ソルト同じパスワードでも、異なるハッシュを生成するランダムな値
RBACロールベースのアクセス制御
bcrypt.hash()登録時にパスワードをハッシュ化する
bcrypt.compare()ログイン時に入力と保存されているハッシュを比較する

認証は、「パスワードが正しいかどうか」だけではなく、パスワードを安全に処理する方法です。パスワードを平文で保存することは災害であり、弱いハッシュ(MD5、SHA-1)は意味がなく、bcryptが現在の標準です。

💬 質問・コメント

0件のコメント

ログインせずに投稿できます。ゲスト投稿は投稿者自身で編集・削除できません。

0/2000

読み込み中...