Help us understand the problem. What is going on with this article?

mongooseを使った認証について考える

More than 3 years have passed since last update.

はじめに

これはただの実装例です。

使うもの

  • mongoose
  • nodeのcrypto (bcryptを使った実装はネット上にあります)

実装例

単純な「ユーザー名」と「パスワード」による認証を実装します。
パスワード生成には crypto.pbkdf2 を使い、そのほかのハッシュアルゴリズムやストレッチ回数などを下記のようなフォーマットで保存することとします。

password: $pbkdf2-digest$iterations$salt$hash

digest: ハッシュアルゴリズム
iterations: ストレッチ回数
salt: ソルト
hash: ハッシュの計算結果

まず、モデルの定義から。

user.js
'use strict';

const crypto = require('crypto');
const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const userSchema = new Schema({
  username: {type: String, required: true, index: {unique: true}},
  password: {type: String, required: true}
});

userSchema.pre('save', function (next) {
  const salt_length = 22;
  const iterations = 100000;
  const key_length = 64;
  const digest = 'sha512';

  // salt生成
  crypto.randomBytes(salt_length, (error, buffer) => {
    if (error) return next(error); // TODO 適切なエラー処理を入れてください

    crypto.pbkdf2(this.password, buffer, iterations, key_length, digest, (error, key) => {
      if (error) return next(error); // TODO 適切なエラー処理を入れてください

      const salt = buffer.toString('hex');
      const hash = key.toString('hex');

      this.password = `$pbkdf2-${digest}$${iterations}$${salt}$${hash}`;
      next();
    });
  });
});

function authenticate(candidate) {
  return new Promise((resolve, reject) => {
    this.findOne({username: candidate.username}, (error, user) => {
      if (error) return reject(error); // TODO 適切なエラー処理を入れてください

      // 解説
      // ここで、userが見つからなかった場合も、処理を続けます。
      // 見つからない場合にすぐに応答を返すと、攻撃者のヒントになりかねないのではないでしょうか。

      const password = user ? user.password : '';
      const re = /^\$(\w+)-(\w+)\$(\d+)\$(\w+)\$(\w+)$/;
      const match = re.exec(password) || [];

      const func = match[1] || 'pbkdf2';
      const digest = match[2] || 'sha512';
      const iterations = match[3] || 100000;
      const salt = match[4] || crypto.randomBytes(22).toString('hex');
      const hash = match[5] || crypto.randomBytes(64).toString('hex');
      const key_length = hash.length / 2;

      crypto[func](candidate.password, new Buffer(salt, 'hex'), parseInt(iterations, 10), key_length, digest, (error, key) => {
        if (error) return reject(error); // TODO 適切なエラー処理を入れてください

        if (key.toString('hex').localeCompare(hash) === 0) {
          resolve(user); // パスワードの一致が確認できた
        } else {
          reject(new Error('ユーザー名、またはパスワードが違います'));
        }
      });
    });
  });
}

// スキーマのstaticなmethodとして定義します。
userSchema.statics.authenticate = authenticate;

mongoose.model('User', userSchema);

実際に使うところのサンプルは、下記のような感じです。

'use strict';

const mongoose = require('mongoose');
require('./user');

mongoose.connect('mongodb://localhost:27017/test', (error) => {
  if (error) throw error; // TODO 適切なエラー処理を入れてください
});

const User = mongoose.model('User');

// ためしにテストユーザーを作ってみます
const testUser = new User({
  username: 'momunchu',
  password: 'password'
});

// saveすることでスキーマに登録していたpre'save'が働き、パスワードがハッシュ化されます
testUser.save(error => {
  if (error) throw error; // TODO 適切なエラー処理を入れてください

  // save後、正しいパスワードでuser情報が得られるか確認してみましょう
  User.authenticate({
    username: 'momunchu',
    password: 'password'
  })
  .then(user => {
    console.log('test1: path');
    console.log(user);
  })
  .catch(error => {
    console.log('test1: NG');
    console.log(error);
  });

  // 間違ったパスワードを入力した場合、エラーとなることを確認しましょう
  User.authenticate({
    username: 'momunchu',
    password: 'パスワード'
  })
  .then(user => {
    console.log('test2: NG');
    console.log(user);
  })
  .catch(error => {
    console.log('test2: path');
    console.log(error);
  });

  // ユーザー名もパスワードもでたらめな場合も、処理時間はなるべく変わらないようにしています
  User.authenticate({
    username: 'モム人',
    password: 'パスワード'
  })
  .then(user => {
    console.log('test3: NG');
    console.log(user);
  })
  .catch(error => {
    console.log('test3: path');
    console.log(error);
  });
});

一度saveすると、二回目以降の実行結果はusernameのduplicateエラーになります。

その他

utf-8でコードを書けば、普通にuft-8のusernameやpasswordでも動きます。

momunchu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away