Аутентификация и Passport.js на Koa
Способы аутентификации обычно называют стратегиями. Их существует очень много:
- через ключ, лежащий на физическом девайсе, как в GitHub делает для пушей и пулов.
- через токены, которые передаются в хедерах http протокола и т.д.
1. Устанавливаем стандартный набор библиотек
На сайте passportjs.org представлено более 500 библиотект для аутентификации пользователя. Нас интересует:
- passport-local – этот модуль позволяет выполнить аутентификацию, используя имя пользователя и пароль в приложениях Node.js. По сути он на сервере производит действия по кодированию и потом (при авторизации) сравнению хешей паролей. Т.е. при регистрации пользователь вводит пароль, этот модуль его кодирует (при помощи секретного произвольного ключа на сервере) и кладет в БД уже закодированным. Потмо при авторизации этот модуль снова при помощи этого кодового слова кодирует введенный пароль и сравнивает с тем, что лежит в БД.
- jwt-simple – для собственно генерации jwt токенов. Можно и другую найти альтернативу. Вообще jwt («джот», хотя часто говорят «дже-ве-те» или «джи-ви-ти») – это JSON Web Tokens, представляющий собой, грубо говоря, кодированный в base64 набор символов (токен) и зашифрованную подпись, по которой сервер может проверить подлинность этого токена потом. JWT нужен для того, чтобы каждый раз не проводить процес авторизации с повтороной отправкой, кодированием и сравнением пароля. Это замена обычным сессиям, которые использовались раньше, но вынуждали обязательно использовать БД для отслеживания авторизации (хотя есть схема и с jwt-токенами, когда задействована БД для хранения рефреш-токена).
Данная библиотека jwt-simple это и делает – создает и кодирует в base64 так называемые заголовок и полезную нагрузку вместе с подписью (это 3 стандартные части jwt-токена). Так получается access-токен, а также к нему создается дополнительно refresh-токен с намного большим сроком жизни для повторного перевыпуска access-токена, если юзер еще не разлогинился. - passport-jwt. Этот модуль позволяет аутентифицировать брекпойнты с помощью веб-токена JSON, т.е. пропускать юзера по урлам, если токен правильный. Он предназначен для использования и для защиты брекпойнтов RESTful без создания обычных сеансов сессий, как раньше делали. Благодаря этой библиотеке наше приложение будет смотреть есть ли в хедере авторизация и соответственно реагировать (говорить «иди авторизируйся» или проверять действительность юзера).
- 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 в целом:
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;
Весь проект вместе с контроллерами для этих роутеров можно найти в моем репозитории на Гитхабе.