Аутентификация в приложениях Node.js с помощью Passport

Реализация надежных систем аутентификации для любого приложения может стать непростой задачей, приложение Node.js здесь не исключение.

В этой статье мы создадим с нуля Node.js приложение и используем относительно новое, но уже очень популярное связующее программное обеспечение - Passport, чтобы разобраться с проблемами аутентификации.

Документация Passport описывает его как "простое, компактное связующее приложение аутентификации для Node" и это верно.


Позиционируясь как связующее программное обеспечение, Passport отлично справляется с тем чтобы выделить среди других элементов веб-приложений именно аспекты аутентификации.

Это позволяет Passport легко настроить любое веб-приложение на базе Express, так же, как мы можем просто настроить другой связующий Express-софт, например, logging, body-parsing, cookie-parsing, session-handling и т.д.

В этой статье мы приведем обзор базовых возможностей фреймворков Node.js и Express, при этом делая упор на вопросы аутентификации. Хотя само приложение Express мы создадим с нуля и расширим его, добавив маршруты и аутентификацию некоторых из этих маршрутов.

Стратегии аутентификации

Passport предлагает нам на выбор свыше 140 механизмов аутентификации. Вы можете проводить аутентификацию с помощью локального/удаленного экземпляра объекта базы данных или использовать единый вход с использованием OAuth, предоставляемый Facebook, Twitter, Google и т.д., для аутентификации в ваших аккаунтах социальных медиа.

Или вы можете выбрать из обширного списка провайдеров, которые поддерживают аутентификацию с помощью Passport и предоставляют для него модуль узла.

Но не беспокоитесь: Вам не нужно включать все стратегии / механизмы, которые вашему приложению и не нужны. Все эти стратегии являются независимыми друг от друга и упакованы в виде отдельных модулей узлов, которые не включаются по умолчанию при установке связующего программного обеспечения Passport: npm install passport.

В этой статье мы будем использовать локальную стратегию аутентификации Passport и аутентификацию пользователей с помощью локально настроенного экземпляра объекта Монго DB, хранящего информацию о пользователе в базе данных.

Для использования стратегии локальной аутентификации, мы должны установить модуль passport-local: npm install passport-local.

Но подождите минутку: перед тем, как вы запустите терминал и начнете выполнение этих команд, давайте все же рассмотрим построение приложения Express с нуля и добавление к нему некоторых маршрутов (для авторизации, регистрации и главной страницы), а затем добавим связующий софт для аутентификации.

Обратите внимание, что в этой статье мы будем использовать Express 4, но с некоторыми незначительными отличиями Passport одинаково хорошо работает с Express 3.

Настройка приложения

Если вы еще этого не сделали, то установите Express и express-generator для создания шаблонных приложений. Для этого нужно просто выполнить в терминале команду express passport-mongo. Сформированная структура приложения должна выглядеть следующим образом:

Настройка приложения

Давайте удалим некоторый функционал по умолчанию, который нам вовсе необязательно использовать - удалите маршрут users.js и уберите все его ссылки из файла app.js.

Добавление зависимостей проекта

Откройте файл package.json и добавьте в него зависимости для passport и модуля passport-local:

"passport": "~0.2.0",
"passport-local": "~1.0.0"

Так как мы будем сохранять информацию о пользователе в MongoDB, мы будем использовать Mongoose в качестве инструмента моделирования данных объекта. Также установить и сохранить зависимость для package.json можно с помощью команды:

npm install mongoose --save

package.json должен выглядеть следующим образом:

Добавление зависимостей проекта

Теперь установите все зависимости и запустите шаблонное приложение, выполнив команду npm install && npm start. Она загружает и устанавливает все зависимости и запускает сервер узла. Вы можете проверить установку приложения Express, перейдя по адресу: http://localhost:3000/, но там вы пока еще ничего особенного не увидите.

Очень скоро мы это изменим, создав полноценное Express приложение, которое запрашивает вывод страницы регистрации для нового пользователя, обрабатывает вход в систему зарегистрированного пользователя и аутентифицирует зарегистрированного пользователя, используя Passport.

Создание модели Mongoose

Так как мы будем сохранять информацию о пользователях в Mongo, давайте создадим модель пользователя в Mongoose и сохраним ее в файле models/user.js нашего приложения:

var mongoose = require('mongoose');

module.exports = mongoose.model('User',{
        username: String,
    password: String,
    email: String,
    gender: String,
    address: String
});

Мы создаем модель Mongoose, с помощью которой мы можем выполнять CRUD операции в основной базе данных.

Конфигурация Mongo

Если у вас нет установленного локально Mongo, мы рекомендуем использовать облачные сервисы баз данных, такие как Modulus или MongoLab.

Создание рабочего экземпляра MongoDB с их помощью не только бесплатно, но кроме того это можно сделать всего лишь в несколько кликов.

После создания базы данных на одном из этих сервисов, вы получите URI базы данных, наподобие mongodb://:@novus.modulusmongo.net:27017/, который можно использовать для выполнения CRUD-операций с базой данных.

Я рекомендую сохранить конфигурацию базы данных в отдельный файл, который можно будет подтянуть, когда это будет необходимо. Таким образом, мы создаем модуль узла db.js, который выглядит следующим образом:

module.exports = {
  'url' : 'mongodb://<dbuser>:<dbpassword>@novus.modulusmongo.net:27017/<dbName>'
}

Я, например, использую локальный экземпляр <b>Mongo</b>, когда мне нужно запустить демон <b>mongod</b>, и <b>db.js</b> должен выглядеть так:

	module.exports = {
  'url' : 'mongodb://localhost/passport'
}

Теперь мы используем эту конфигурацию в app.js и подключаемся к ней с помощью Mongoose API:

var dbConfig = require('./db.js');
var mongoose = require('mongoose');
mongoose.connect(dbConfig.url);

Конфигурация Passport

Passport только обеспечивает механизм для обработки аутентификации, не заботясь о реализации обработки своей же сессии, для этого мы будем использовать express-session.

Откройте файл app.js и вставьте перед настройкой маршрутов приведенный ниже код:

// Конфигурация Passport
var passport = require('passport');
var expressSession = require('express-session');
app.use(expressSession({secret: 'mySecretKey'}));
app.use(passport.initialize());
app.use(passport.session());

Это необходимо, поскольку мы хотим, чтобы сеансы пользователей были стабильными. Перед запуском приложения мы должны установить express-session и добавить его в список зависимостей файла package.json. Для этого наберите в командной строке - npm install --save express-session.

Сериализация и десериализация экземпляров объекта пользователя

Для Passport также необходима сериализация и десериализация экземпляра объекта пользователя из сессии сохранения в целях поддержки текущей сессии, так чтобы каждый последующий запрос не содержал учетные данные пользователя. Для этого предназначены два метода serializeUser и deserializeUser:

passport.serializeUser(function(user, done) {
  done(null, user._id);
});

passport.deserializeUser(function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
});

Использование стратегий Passport

Теперь мы определим стратегии Passport для обработки авторизации и регистрации. Каждая из них будет экземпляром стратегии локальной аутентификации Passport и будет создаваться при помощи функции passport.use().

Мы используем connect-flash, что поможет нам в обработке ошибок, предоставляя флэш-сообщения, которые могут выводиться пользователю при возникновении ошибки.

Стратегия авторизации

Стратегия авторизации выглядит следующим образом:

// passport/login.js
passport.use('login', new LocalStrategy({
    passReqToCallback : true
  },
  function(req, username, password, done) { 
    // проверка в mongo, существует ли пользователь с таким логином
    User.findOne({ 'username' :  username }, 
      function(err, user) {
        // В случае возникновения любой ошибки, возврат с помощью метода done
        if (err)
          return done(err);
        // Пользователь не существует, ошибка входа и перенаправление обратно
        if (!user){
          console.log('User Not Found with username '+username);
          return done(null, false, 
                req.flash('message', 'User Not found.'));                 
        }
        // Пользователь существует, но пароль введен неверно, ошибка входа 
        if (!isValidPassword(user, password)){
          console.log('Invalid Password');
          return done(null, false, 
              req.flash('message', 'Invalid Password'));
        }
        // Пользователь существует и пароль верен, возврат пользователя из 
        // метода done, что будет означать успешную аутентификацию
        return done(null, user);
      }
    );
}));

Первый параметр passport.use() является именем стратегии, которое будет использоваться для идентификации этой стратегии при последующем применении. Вторым параметром является тип стратегии, которую вы хотите создать, здесь мы используем username-password или LocalStrategy.

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

Переменная конфигурации passReqToCallback позволяет нам получить доступ к объекту request в функции обратного вызова, благодаря чему, в свою очередь, мы имеем возможность использовать любой параметр, связанный с запросом.

Далее, мы используем Mongoose API, чтобы найти пользователя в нашей основной базе пользователей и проверить, является ли он доверенным пользователем или нет.

Последний параметр в нашем обратном вызове done указывает на используемый метод, с помощью которого мы сообщаем модулю Passport об успешном выполнении действия или ошибке.

Чтобы идентифицировать сбой либо первый параметр должен содержать ошибку, либо второй параметр должен содержать значение false. Для обозначения успешного прохождения действия первый параметр должен иметь значение null, а второй - truthy, в этом случае объект request становится доступен.

Поскольку пароли по своей природе являются уязвимым местом, мы всегда должны шифровать их перед сохранением в базу данных. Для этого мы используем bcrypt-nodejs, который помогает шифровать и расшифровывать пароли:

var isValidPassword = function(user, password){
  return bCrypt.compareSync(password, user.password);
}

Если вам неудобно работать с отдельными фрагментами кода, и вы предпочитают видеть полный код в действии, вы можете просмотреть его здесь.

Стратегия регистрации

Теперь мы определяем следующую стратегию, которая будет обрабатывать регистрацию нового пользователя и создавать его учетную запись в основной базе данных Mongo DB:

passport.use('signup', new LocalStrategy({
    passReqToCallback : true
  },
  function(req, username, password, done) {
    findOrCreateUser = function(){
      // поиск пользователя в Mongo с помощью предоставленного имени пользователя
      User.findOne({'username':username},function(err, user) {
        // В случае любых ошибок - возврат
        if (err){
          console.log('Error in SignUp: '+err);
          return done(err);
        }
        // уже существует
        if (user) {
          console.log('User already exists');
          return done(null, false, 
             req.flash('message','User Already Exists'));
        } else {
          // если пользователя с таки адресом электронной почты
          // в базе не существует, создать пользователя
            var newUser = new User();
          // установка локальных прав доступа пользователя
          newUser.username = username;
          newUser.password = createHash(password);
          newUser.email = req.param('email');
          newUser.firstName = req.param('firstName');
          newUser.lastName = req.param('lastName');

          // сохранения пользователя
          newUser.save(function(err) {
            if (err){
              console.log('Error in Saving user: '+err);  
              throw err;  
            }
            console.log('User Registration succesful');    
            return done(null, newUser);
          });
        }
      });
    };

    // Отложить исполнение findOrCreateUser и выполнить 
    // метод на следующем этапе цикла события
    process.nextTick(findOrCreateUser);
  });
);

Здесь мы снова использовали Mongoose API, чтобы выяснить, существует ли пользователь с данным именем пользователя или нет. Если нет, то создаем нового пользователя и сохраняем информацию о нем в Mongo.

В противном случае возвращаем ошибку с помощью обратного вызова done и флэш-сообщения. Обратите внимание, что мы используем bcrypt-nodejs для создания хэша пароля перед его сохранением:

// Генерация хэша с помощью bCrypt
var createHash = function(password){
 return bCrypt.hashSync(password, bCrypt.genSaltSync(10), null);
}

Создание маршрутов

В общем, схема нашего приложения будет выглядеть так:

Создание маршрутов

Теперь мы задаем маршруты для применения в следующем модуле, который принимает экземпляр паспорта созданного ранее в app.js. Сохраните этот модуль в файле routes/index.js:

module.exports = function(passport){

  /* Получение страницы авторизации. */
  router.get('/', function(req, res) {
    // Вывод страницы авторизации со всеми флэш-сообщениями, если
    // таковые существуют
    res.render('index', { message: req.flash('message') });
  });

  /* Обработка POST-данных авторизации */
  router.post('/login', passport.authenticate('login', {
    successRedirect: '/home',
    failureRedirect: '/',
    failureFlash : true 
  }));

  /* Получение страницы регистрации */
  router.get('/signup', function(req, res){
    res.render('register',{message: req.flash('message')});
  });

  /* Обработка регистрационных POST-данных */
  router.post('/signup', passport.authenticate('signup', {
    successRedirect: '/home',
    failureRedirect: '/signup',
    failureFlash : true 
  }));

  return router;
}

Наиболее важной частью приведенного выше фрагмента кода является использование passport.authenticate() для делегирования аутентификации стратегиям login и signup, когда HTTP POST выполнен для маршрутов /login и /signup соответственно.

Обратите внимание, что не обязательно называть стратегии соответственно пути маршрута, им можно назначать произвольные имена.

Создание представлений Jade

Далее, мы создаем следующие два представления для нашего приложения:

  • layout.jade - содержит базовую структуру и информацию о стилях;
  • index.jade - содержит страницу авторизации, включающую форму входа и опции для создания новой учетной записи:
extends layout

block content
  div.container
    div.row
      div.col-sm-6.col-md-4.col-md-offset-4
        h1.text-center.login-title Sign in to our Passport app
          div.account-wall
            img(class='profile-img', src='https://lh5.googleusercontent.com/-b0-k99FZlyE/AAAAAAAAAAI/AAAAAAAAAAA/eu7opA4byxI/photo.jpg?sz=120')
            form(class='form-signin', action='/login', method='POST')
              input(type='text', name='username' class='form-control', placeholder='Email',required, autofocus)
              input(type='password', name='password' class='form-control', placeholder='Password', required)
              button(class='btn btn-lg btn-primary btn-block', type='submit') Sign in
              span.clearfix
          a(href='/signup', class='text-center new-account') Create an account
          #message
          if message
            h1.text-center.error-message #{message}

Благодаря Bootstrap наша страница авторизации теперь выглядит следующим образом:

Создание представлений Jade

Нам также потребуются еще два представления для ввода регистрационных данных и для домашней страницы приложения:

  • register.jade - содержит форму регистрации;
  • home.jade - выводит приветствие и данные авторизовавшегося пользователя.

Если вы не работали с Jade, здесь можете найти документацию по нему.

Реализация функции выхода из системы

Passport, будучи связующим софтом, позволяет добавлять определенные свойства и методы к объектам запросов и ответов, что в свою очередь дает возможность добавить очень удобный метод request.logout(), который отменяет сеанс пользователя:

/* Handle Logout */
router.get('/signout', function(req, res) {
  req.logout();
  res.redirect('/');
});

Защита маршрутов

Passport также предоставляет возможность защитить доступ к маршруту, который не предназначен для анонимных пользователей.

Это означает, что если некоторые пользователи пытается получить доступ к маршруту http://localhost:3000/home без авторизации в системе, они будут перенаправлены на главную страницу следующим образом:

/* Получение домашней страницы */
router.get('/home', isAuthenticated, function(req, res){
  res.render('home', { user: req.user });
});

// Так как любое связующее программное обеспечение базируется
// на вызовах next(), если пользователь аутентифицирован:
var isAuthenticated = function (req, res, next) {
  if (req.isAuthenticated())
    return next();
  res.redirect('/');
}

Заключение

Passport не является единственным возможным решением, когда речь заходит об аутентификации приложений Node.js, существуют и альтернативные варианты, такие как EveryAuth.

Но большое количество модулей, гибкость, поддержка сообщества и тот факт, что он является просто связующим программным обеспечением, действительно выделяет Passport из череды других приложений.

Сергей Бензенкоавтор-переводчик статьи «Authenticating Node.js Applications With Passport»