+5

🔐Node.js Expressで安全なパスワードリセット機能を実装する

1. はじめに

この記事では、Node.js Expressアプリケーションで安全なパスワードリセット機能を実装する方法について説明します。パスワードリセットは、セキュリティと良好なユーザーエクスペリエンスを確保するための重要な機能です。これを実現するために、環境の設定からリセットメールの送信、最終的にユーザーのパスワードの更新までのプロセスを段階的に説明します。

2. プロジェクトの設定

2.1 プロジェクトの初期化

まず、プロジェクト用の新しいフォルダを作成し、コマンドラインでそのフォルダに移動します。次に、以下のコマンドを実行して、package.jsonファイルでプロジェクトを初期化します。

npm init -y

2.2 依存関係のインストール

次に、以下のコマンドを実行して、このプロジェクトに必要な依存関係をインストールします。

npm install express mongoose bcryptjs jsonwebtoken nodemailer dotenv

これらの依存関係には、以下が含まれます。

  • express:コアとなるExpressフレームワーク
  • mongoose:Node.js用のMongoDBオブジェクトモデリングライブラリ
  • bcryptjs:パスワードのハッシュ化と比較のためのライブラリ
  • jsonwebtoken:JSON Webトークンを生成・検証するためのライブラリ
  • nodemailer:メールを送信するためのモジュール
  • dotenv:.envファイルから環境変数を読み込むためのモジュール

3. 環境の設定

3.1 .envファイルの作成

プロジェクトのルートに.envファイルを作成し、機密データや環境固有の設定を保存します。ファイルに以下の行を追加します。

MONGODB_URI=mongodb://localhost:27017/password-reset
EMAIL_SERVICE=あなたのメールサービス
EMAIL_USER=あなたのメールアドレス
EMAIL_PASS=あなたのメールパスワード
JWT_SECRET=あなたのJWT秘密鍵

プレースホルダーを、メールサービスおよびアカウントの資格情報に適した値に置き換えてください。

3.2 環境変数の読み込み

メインアプリケーションファイル(例:app.js)で、dotenvモジュールをインポートし、.envファイルから環境変数を読み込むように設定します。

require('dotenv').config();

4. データベースの設定

4.1 MongoDBへの接続

Mongooseを使用して、MongoDBデータベースへの接続を確立します。app.jsファイルを以下のコードで更新します。

const express = require('express');
const mongoose = require('mongoose');

const app = express();
app.use(express.json());

mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() => console.log('Connected to MongoDB'))
  .catch((err) => console.error('Failed to connect to MongoDB:', err));

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));

4.2 ユーザースキーマとモデルの定義

modelsという新しいフォルダを作成し、その中にUser.jsというファイルを作成します。次のコードでユーザースキーマとモデルを定義します。

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  password: { type: String, required: true },
  passwordResetToken: { type: String },
  passwordResetExpires: { type: Date },
});

// データベースに保存する前にパスワードをハッシュ化する
userSchema.pre('save', async function (next) {
  if (!this.isModified('password')) return next();
  const salt = await bcrypt.genSalt(10);
  this.password = await bcrypt.hash(this.password, salt);
  next();
});

// ユーザーのパスワードを検証するインスタンスメソッド
userSchema.methods.validatePassword = async function (password) {
  return await bcrypt.compare(password, this.password);
};

const User = mongoose.model('User', userSchema);
module.exports = User;

5. パスワードリセット機能の実装

5.1 ルートの設定

routesという新しいフォルダを作成し、その中にauth.jsというファイルを作成します。以下のルートを設定します。

  • /auth/forgot-password:パスワードリセットプロセスを開始するため
  • /auth/reset-password:実際のパスワードリセットを処理するため
const express = require('express');
const router = express.Router();
const { forgotPassword, resetPassword } = require('../controllers/authController');

router.post('/forgot-password', forgotPassword);
router.post('/reset-password', resetPassword);

module.exports = router;

次に、app.jsファイルでauth.jsルータをインポートして使用します。

const authRoutes = require('./routes/auth');

app.use('/auth', authRoutes);

5.2 Authコントローラの作成

controllersという新しいフォルダを作成し、その中にauthController.jsというファイルを作成します。このファイルには、パスワードリセットプロセスを処理するためのコントローラ関数が含まれます。

const User = require('../models/User');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const nodemailer = require('nodemailer');

const transporter = nodemailer.createTransport({
  service: process.env.EMAIL_SERVICE,
  auth: {
    user: process.env.EMAIL_USER,
    pass: process.env.EMAIL_PASS,
  },
});

async function forgotPassword(req, res) {
  // TODO: forgotPassword関数を実装する
}

async function resetPassword(req, res) {
  // TODO: resetPassword関数を実装する
}

module.exports = {
  forgotPassword,
  resetPassword,
};

5.3 forgotPassword関数の実装

authController.jsファイルで、パスワードリセット要求を処理するためのforgotPassword関数を実装します。

async function forgotPassword(req, res) {
    // メールでユーザーを見つける
    const user = await User.findOne({ email: req.body.email });

    if (!user) {
        return res.status(404).json({ error: 'ユーザーが見つかりません' });
    }

    // パスワードリセットトークンを生成し、有効期限を設定する
    const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: '1h' });
    user.passwordResetToken = token;
    user.passwordResetExpires = Date.now() + 3600000; // 1時間
    await user.save();

    // パスワードリセットメールを送信する
    const resetUrl = `http://${req.headers.host}/auth/reset-password?token=${token}`;
    const mailOptions = {
        from: process.env.EMAIL_USER,
        to: user.email,
        subject: 'パスワードリセットリクエスト',
        html: `
        <p>パスワードリセットをリクエストしました。以下のリンクをクリックしてパスワードをリセットしてください。<\p>
        <a href="${resetUrl}">${resetUrl}<\a>
        <p>このリクエストを行っていない場合は、このメールを無視してください。<\p>
        `,
    };

    try {
        await transporter.sendMail(mailOptions);
        res.json({ message: 'パスワードリセットメールが送信されました' });
    } catch (err) {
        console.error('パスワードリセットメールの送信に失敗しました:', err);
        res.status(500).json({ error: 'パスワードリセットメールの送信に失敗しました' });
    }
}

5.4 resetPassword関数の実装

authController.jsファイルで、パスワードリセットの確認とユーザーのパスワードの更新を処理するためのresetPassword関数を実装します。

async function resetPassword(req, res) {
  // パスワードリセットトークンを検証する
  const token = req.query.token;
  const decodedToken = jwt.verify(token, process.env.JWT_SECRET);

  // IDとトークンでユーザーを検索し、トークンがまだ有効かどうかを確認する
  const user = await User.findOne({
    _id: decodedToken.id,
    passwordResetToken: token,
    passwordResetExpires: { $gt: Date.now() },
  });

  if (!user) {
    return res.status(401).json({ error: '無効または期限切れのパスワードリセットトークン' });
  }

  // ユーザーのパスワードを更新し、リセットトークンとその有効期限を削除する
  user.password = req.body.password;
  user.passwordResetToken = undefined;
  user.passwordResetExpires = undefined;
  await user.save();

  // 確認メールを送信する
  const mailOptions = {
    from: process.env.EMAIL_USER,
    to: user.email,
    subject: 'パスワードリセット確認',
    html: `
      <p>パスワードが正常にリセットされました。このリクエストを行っていない場合は、すぐにお問い合わせください。</p>
    `,
  };

  try {
    await transporter.sendMail(mailOptions);
    res.json({ message: 'パスワードリセットが成功しました' });
  } catch (err) {
    console.error('パスワードリセット確認メールの送信に失敗しました:', err);
    res.status(500).json({ error: 'パスワードリセット確認メールの送信に失敗しました' });
  }
}

6. 実装のテスト

パスワードリセット機能をテストするには、Postmanやcurlなどのツールを使用して、/auth/forgot-passwordおよび/auth/reset-passwordエンドポイントにリクエストを送信します。以下は、Postmanを使用してリクエストを送信する手順です。

  1. Postmanを開き、新しいリクエストを作成します。
  2. リクエストタイプをPOSTに設定し、http://localhost:3000/auth/forgot-passwordをURLに入力します。
  3. Bodyタブを選択し、rawを選択して、JSONを選択します。
  4. ボディに次のJSONオブジェクトを入力し、登録済みのユーザーのメールアドレスを指定します。
    {
      "email": "user@example.com"
    }
    
  5. Sendボタンをクリックしてリクエストを送信します。正常なレスポンスが返され、指定したメールアドレスにパスワードリセットメールが送信されます。
  6. メールに記載されているリセットリンクをコピーし、Postmanで新しいリクエストを作成します。
  7. リクエストタイプをPOSTに設定し、http://localhost:3000/auth/reset-password?token=コピーしたトークンをURLに入力します。
  8. 再びBodyタブを選択し、rawを選択して、JSONを選択します。
  9. ボディに次のJSONオブジェクトを入力し、新しいパスワードを指定します。
    {
      "password": "new-password"
    }
    
  10. Sendボタンをクリックしてリクエストを送信します。正常なレスポンスが返され、パスワードがリセットされます。

7. まとめ

この記事では、Node.js Expressアプリケーションで安全なパスワードリセット機能を実装する方法について説明しました。これにより、ユーザーはパスワードを忘れた場合でも、セキュアな方法でアカウントへのアクセスを回復できます。

今回実装した方法は一例であり、実際のプロダクション環境では、さらにエラーハンドリングやバリデーションを追加することが望ましいです。また、フロントエンドでパスワードリセットのUIを構築し、ユーザーにより良いエクスペリエンスを提供することも重要です。

Mình hy vọng bạn thích bài viết này và học thêm được điều gì đó mới.

Donate mình một ly cafe hoặc 1 cây bút bi để mình có thêm động lực cho ra nhiều bài viết hay và chất lượng hơn trong tương lai nhé. À mà nếu bạn có bất kỳ câu hỏi nào thì đừng ngại comment hoặc liên hệ mình qua: Zalo - 0374226770 hoặc Facebook. Mình xin cảm ơn.

Momo: NGUYỄN ANH TUẤN - 0374226770

TPBank: NGUYỄN ANH TUẤN - 0374226770 (hoặc 01681423001)

image.png


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí