Аутентификация и Passport.js на Koa

Способы аутентификации обычно называют стратегиями. Их существует очень много:

  • через ключ, лежащий на физическом девайсе, как в GitHub делает для пушей и пулов.
  • через токены, которые передаются в хедерах http протокола и т.д.

1. Устанавливаем стандартный набор библиотек

На сайте passportjs.org представлено более 500 библиотект для аутентификации пользователя. Нас интересует:

  1. passport-local – этот модуль позволяет выполнить аутентификацию, используя имя пользователя и пароль в приложениях Node.js. По сути он на сервере производит действия по кодированию и потом (при авторизации) сравнению хешей паролей. Т.е. при регистрации пользователь вводит пароль, этот модуль его кодирует (при помощи секретного произвольного ключа на сервере) и кладет в БД уже закодированным. Потмо при авторизации этот модуль снова при помощи этого кодового слова кодирует введенный пароль и сравнивает с тем, что лежит в БД.
  2. jwt-simple – для собственно генерации jwt токенов. Можно и другую найти альтернативу.  Вообще jwt (“джот”, хотя часто говорят “дже-ве-те” или “джи-ви-ти”) – это JSON Web Tokens, представляющий собой, грубо говоря, кодированный в base64 набор символов (токен) и зашифрованную подпись, по которой сервер может проверить подлинность этого токена потом. JWT нужен для того, чтобы каждый раз не проводить процес авторизации с повтороной отправкой, кодированием и сравнением пароля. Это замена обычным сессиям, которые использовались раньше, но вынуждали обязательно использовать БД для отслеживания авторизации (хотя есть схема и с jwt-токенами, когда задействована БД для хранения рефреш-токена).
    Данная библиотека jwt-simple это и делает – создает и кодирует в base64 так называемые заголовок и полезную нагрузку вместе с подписью (это 3 стандартные части jwt-токена). Так получается access-токен, а также к нему создается дополнительно refresh-токен  с намного большим сроком жизни для повторного перевыпуска access-токена, если юзер еще не разлогинился.
  3. passport-jwt. Этот модуль позволяет аутентифицировать брекпойнты с помощью веб-токена JSON, т.е. пропускать юзера по урлам, если токен правильный. Он предназначен для использования и для защиты брекпойнтов RESTful без создания обычных сеансов сессий, как раньше делали. Благодаря этой библиотеке наше приложение будет смотреть есть ли в хедере авторизация и соответственно реагировать (говорить “иди авторизируйся” или проверять действительность юзера).
  4. koa-pasport – “собирает” все это в кучу и подключает к нашему приложению.

2. Создаем файлы с настройками наших стратегий

В папке src/libs/passport создаем 3 файла:

  • koaPassport.js
  • localStrategy.js
  • jwtStrategy.js

В доках для passport-local есть небольшой пример использования в несколько строк.

Из него можно заметить, что passport.use – это что-то на подобие мидлвары app.use. Таким образом подключаем локальную стратегию. Принимаемая, помимо логина и  пароля,  функция done говорит нам о том, прошла ли аутентификация или нет, зависимо от того, что мы в нее кидаем.

Но тот примитивный в доках пример не пригодный для использования в таком упрощенном виде. Поэтому берем код ниже и вставляем в localStrategy.js:

(здесь происходит создание токенов)

const LocalStrategy = require('passport-local'); //импортировали собственно нашу стратегию
const jwt = require('jwt-simple');

const { UserDB } = require('../../models/user/UserDB'); //интерфейс для общения с БД (еще не создали)

const opts = { // объявили опшини для передачив нашу стратегию
  usernameField: 'email',
  passwordField: 'password',
  passReqToCallback: true, //включает колбек, который идет ниже после слова async
  session: false, //говорит, есть ли у нас сессии (в нашем примере нет)
};

module.exports = new LocalStrategy(opts, async (req, email, password, done) => {
  UserDB.checkPassword(email, password).then((checkPasswordResponse) => {
    if (!checkPasswordResponse.flag) {
      return done({ message: checkPasswordResponse.message }, false);
    }

    const { user } = checkPasswordResponse;

// ЭТА ЧАСТЬ НИЖЕ СОБСТВЕННО ОТВЕЧАЕТ ЗА JWT И БЕЗ НЕЕ ПОСТОЯННО БУДЕТ НУЖНА АУТЕНТИФИКАЦИЯ

 
    const accessToken = { //тело access токена, куда кладем id, что потом будет обрабатываться в файле jwt стратегии
      id: user.id,
      expiresIn: new Date().setTime(new Date().getTime() + 200000),
    };
    const refreshToken = { //в refresh токена кладем емейл, по нему будет обновляться по истечении времени accessToken
      email: user.email,
      expiresIn: new Date().setTime(new Date().getTime() + 1000000),
    };

    user.tokens = {
      accessToken: jwt.encode(accessToken, 'super_secret'),
      accessTokenExpirationDate: accessToken.expiresIn,
      refreshToken: jwt.encode(refreshToken, 'super_secret_refresh'),
      refreshTokenExpirationDate: refreshToken.expiresIn,
    };
// КОНЕЦ ЧАСТИ ОТВЕЧАЮЩЕЙ ЗА JWT

    return done(null, checkPasswordResponse.user);
  }).catch((err) => done({ message: err.message }, false));
});

jwtStrategy.js:

(здесь мы проверяем наличие токена и реагируем на это соответственно)

const JwtStrategy = require('passport-jwt').Strategy;
const { ExtractJwt } = require('passport-jwt');

const { UserDB } = require('../../models/user/User');

const opts = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('JWT'),
  secretOrKey: 'super_secret', 
};

module.exports = new JwtStrategy(opts, (jwtPayload, done) => {
  if (jwtPayload.expiresIn <= new Date().getTime()) { //проверяет, вышло ли время токена и реагируем
    done({ isPassport: true, message: 'Expired access token.' }, false);
  }

  UserDB.getUserById(jwtPayload.id) //по айдищнику (он тоже в токене) отдаем юзера
    .then((user) => done(null, user))
    .catch((err) => done({ isPassport: true, message: err.message }, false));
});

В koaPassport.js создаем сам наш пасспорт, подключив стратегии, и экспортируем его:

(здесь просто “собираем все в кучу”)

const passport = require('koa-passport');

passport.use(require('./jwtStrategy'));
passport.use(require('./localStrategy'));

module.exports = passport;

3. Создаем модель для общения с БД

В localStrategy.js выше мы указали ссылку на модель для общения с БД (UserDB). А теперь давайте ее создадим. Это облегчит взаемодействие с БД и избавит от необходимости каждый раз писать запрос, проверять его и т.д. Благодаря созданию этого интерфейса мы сможем поместить все в одну строчку, там где будем использовать данный код.

src/models/user/UserDB.js:

const crypto = require('crypto');

const db = require('../../db/db');

class UserDB {
  static async getUserById(id) {  //проверяем наличие юзера в БД
    const userResponse = await db.query(`SELECT * FROM "user" WHERE id = ${id}`); 

    if (!userResponse.rowCount) {
      throw new Error(`User with id: ${id}, does not exist`);
    }

    return userResponse.rows[0];
  }

  static async getUserByEmail(email) {
    const userResponse = await db.query(`SELECT * FROM "user" WHERE email = '${email}'`);

    if (!userResponse.rowCount) {
      throw new Error(`User with email: ${email}, does not exist`);
    }

    return userResponse.rows[0];
  }

  static async checkPassword(email, password) {
    const userResponse = await db.query(`SELECT * FROM "user" WHERE email = '${email}'`);

    if (!userResponse.rowCount) {
      return { message: `User with email: ${email}, does not exist`, flag: false };
    }

    const user = { ...userResponse.rows[0] };

    if (crypto.pbkdf2Sync(password, 'salt', 100000, 64, 'sha256').toString('hex') !== user.password) {
      return { message: 'Incorect password', flag: false };
    }

    return { user, flag: true };
  }
}

module.exports = { UserDB };

4. Подключаем использование passport в наше приложение

В файле app.js импортируем наш созданный passport из koaPassport (где все части стратегии “собирали в кучу”) и инициализируем его:

const passport = require('./libs/passport/koaPassport')

passport.initialize();

Вот так может примерно выглядеть сам файл app.js в целом:

Подключения passport в app.js Koa

5. Настраиваем роутер и контроллер

В файле router.js тоже импортируем установленную библиотеку koa-passport. И объявляем роут для логинизации пользователя, например, sign-in с подключением контроллера signIn:

const Router = require('koa-joi-router');
const passport = require('koa-passport'); // импортировали нашу стратегию

const { UsersController } = require('./users.controller');
const UsersValidator = require('./users.validator');

const router = new Router();

router.get('/:userId', UsersController.getUser);
router.get('/', UsersController.getUsersList);
router.post('/sign-up', UsersValidator.signUp, UsersController.createUser);
router.delete('/', UsersController.deleteUser);
router.post('/sign-in', UsersValidator.signIn, UsersController.signIn);
router.get('/profile', passport.authenticate('jwt', { session: false }), UsersController.profile); //пример роута с необходимой аутентификацией
router.get('/refresh/token', UsersController.refreshToken);
router.post('/category-id', UsersValidator.getUsersFromCategoryById, UsersController.getUsersFromCategoryById);
router.post('/category-name', UsersValidator.getUsersFromCategoryByName, UsersController.getUsersFromCategoryByName);
router.put('/photo', passport.authenticate('jwt', { session: false }), UsersController.updatePhoto); //пример роута с необходимой аутентификацией

module.exports = router;

Весь проект вместе с контроллерами для этих роутеров можно найти в моем репозитории на Гитхабе.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *