На прошлой неделе я рассказал о планировании маршрута для Node.js/Express/MongoDB API. На этой неделе я расскажу об одном подходе к защите API, который включает хэширование паролей пользователей и создание токенов для аутентификации запросов API от клиента. Другим решением этой проблемы может быть использование службы аутентификации Google FireBase, которая позаботится о входе в систему, создании и проверке токена сеанса из коробки. Хотя такой подход «под ключ» выглядит заманчиво, я думаю, что построение системы безопасности с нуля важно для самостоятельного понимания основ.

Я устанавливаю два пакета для обеспечения безопасности:

BCrypt используется для создания и проверки хешированного пароля. Хэшированные пароли нельзя использовать, если ваша база данных взломана и хешированные пароли раскрыты, но их можно использовать для проверки отправленных паролей пользователей.

JWT используется для создания и проверки токенов. Сами токены состоят из трех частей, разделенных знаком .. Первая часть содержит параметры конфигурации. Вторая часть содержит фактическую полезную нагрузку (в нашем случае идентификатор пользователя), а третья — вашу зашифрованную подпись. Скопируйте токен ниже и вставьте в отладчик jwt.io, и вы увидите отображаемый идентификатор пользователя:

token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI1ZmJiZThmN2MyMDczZTYwMjllOTE5YTAiLCJpYXQiOjE2MDYzMTMyNDN9.4xu_4bWNVGHbtxJAwB5xhdOaSrvUnQZFNaP0HDyWT5o
userId:
"userId": "5fbbe8f7c2073e6029e919a0"

Так какой в ​​этом смысл, если вы можете получить доступ к полезной нагрузке без секрета? Вы также должны увидеть сообщение «Invalid Secret». Так работает токен. Полезная нагрузка не предназначена для хранения конфиденциальной информации, но токен не пройдет проверку, если не будет включена правильная подпись.

Установив эти два инструмента, мы настраиваем маршруты. Каждый базовый маршрут использует функцию безопасности по-разному:

Из этой диаграммы видно, что токены создаются и отправляются, когда 1) создается новый пользователь (его пароль также будет хеширован и сохранен) и 2) когда существующий пользователь входит в систему (пароль подтвержден). Токен создается и отправляется клиенту, где он хранится в локальном хранилище (или в хранилище сеансов, или в хранилище файлов cookie). Размещение токена в локальном хранилище позволит токену сохраняться между сеансами браузера, поэтому пользователю не нужно входить в систему каждый раз, когда он возвращается на сайт. Когда приложение посещается и существует токен, он будет отправлен (в заголовке запроса на серверную часть, которая, если она будет проверена, позволит серверу отправить конкретную информацию о пользователе из базы данных для заполнения браузера — получить профиля или конечной точки автоматического входа в систему.

Сохраненные токены также используются для проверки того, что пользователь уполномочен делать запросы к определенным конечным точкам, тем самым защищая права на чтение и/или запись конфиденциальных данных.

Функции безопасности отделяются от маршрутизаторов, а затем вызываются из самих маршрутизаторов по мере выполнения запросов к API, в зависимости от типа запроса.

Создайте и сохраните хешированный пароль, используя метод Mongoose Schema.pre в файле модели пользователя. Это будет вызвано при создании нового пользователя или если пользователь изменит свой пароль. Функция вызывает метод Bcrypt.hash(), который принимает пароль и количество раундов соли в качестве аргументов. При добавлении соли в пароль добавляются случайные символы, что затрудняет угадывание хэша. Соленые символы встроены в хэш, и их не нужно хранить отдельно. Чем больше количество раундов, тем безопаснее, но с компромиссом в том, что проверка занимает больше времени.

// src/models/user.js
userSchema.pre('save', async function() {
  const user = this; // So we can refer to user instead of this
  if (user.isModified('password')) 
    user.password = bcrypt.hash(user.password, 8);
  }
  next()
});

Создайте JWT на основе идентификатора пользователя. Это произойдет в методе Mongoose Schema.methods (метод экземпляра), который может быть вызван маршрутизатором. Функция вызывает jwt.sign() с идентификатором пользователя и вашей секретной подписью в качестве аргументов.

// src/models/user.js
UserSchema.methods.generateAuthToken = async function () {
  const user = this;
  const JWTSecret = 'secret';
  const token = await jwt.sign({ userId: user._id.toString() },   JWTSecret);
  user.tokens = user.tokens.concat({ token });
  await user.save();
  return token;
};

Подтвердите адрес электронной почты, пароль и получите пользователя. Это произойдет с использованием метода Mongoose Schema.statics (метод класса), который может быть вызван маршрутизатором. Функция вызывает bcrypt.compare(), которая принимает сохраненный хэш и отправленный пароль. Пароль хэшируется, и два хэша сравниваются.

// src/models/user.js
UserSchema.statics.findByCredentials = async (email, password) => {
  const user = await User.findOne({ email });
  if (!user) {
    throw new Error('Unable to login.');
  }
  const isMatch = await bcrypt.compare(password, user.password);
  if (!isMatch) {
    throw new Error('Unable to login.');
  }
  return user;
};

Авторизуйте отправленный токен перед запуском обработчика маршрута. Это Express Middleware, которое может вызываться на всех маршрутах, которые необходимо авторизовать. Функция вызывает jwt.verify() и принимает два аргумента: отправленный токен и секрет подписи. В качестве бонуса, как только токен и пользователь проверены, пользователь возвращается к маршрутизатору, избегая необходимости выполнять второй запрос. По сути, возвращая текущего пользователя при каждом успешном вызове аутентификации.

// src/middleware/authentication.js
const auth = async (req, res, next) => {
  try {
    const sentToken = req.headers.authorization.replace('Bearer ',    '');
    const JWTSecret = 'secret';
    const decoded = jwt.verify(sentToken, JWTSecret);
// The following checks to see if user exists AND if that user has the correct token.
    const user = await User.findOne({ _id: decoded.userId, 'tokens.token': sentToken });
    if (!user) {
      throw new Error();
    }
    req.user = user;
    next();
  } catch (error) {
    res.status(400).send({ error: 'Please login.' });
  }
};

Эта настройка обеспечивает хорошую базовую настройку безопасности для вашего экспресс-приложения. Еще одна важная деталь. Убедитесь, что вы не раскрываете свой секрет JWT в коде вашего клиента, так как это создает огромный риск для безопасности. Скорее секрет должен быть скрыт в переменной окружения.