Хабрахабр

Еще раз о passport.js

Недавно мне передали на поддержку проект на express.js. При изучении кода проекта я обнаружил немного запутанную работу с аутентификацией/авторизацией которая базировалась, как и 99,999% случаев, на библиотеке passport.js. Этот код работал, и следуя принципу «работет — не трогай», я оставил его как есть. Когда через пару дней мне дали задание добавить еще две стратегии авторизации. И тогда я начал вспоминать, что уже делал аналогичную работу, и это занимало несколько строк кода. Полистав документацию на сайте passport.js, я почти не сдвинулся с места в понимании того, что и как нужно делать, т.к. там рассматривались случаи, когда используется ровно одна стратегия, для которой, для каждой в отдельности, и даются примеры. Но как соединить несколько стратегий, зачем нужно использовать метод logIn() (что то же самое, что login()) — по-прежнему не прояснялось. Поэтому, чтобы разобраться сейчас, и не повторять тот же поиск еще и еще раз, — я составил для себя эти заметки.
Немного истории. Изначально веб-приложениях использовали два вида аутентификации/авторизации: 1) BASE и 2) при помощи сессий с использованием cookie. В BASE аутентификации/авторизации в каждом запросе передается некоторый заголовок, и, таким образом, в каждом запросе проводится аутентификация клиента. При использовании сессий, аутентификация клиента проводится только один раз (способы могут быть самые различные в том числе и BASE, а еще по имени и паролю, которые отправляются в форме, и еще тысячи других способов, которые в терминах passport.js называются стратегиями). Главное, что после прохождения аутентификации, клиенту в cookie сохраняется идентификатор сессии (или в некоторых реализациях данные сессии), а в данных сессии сохраняется идентификатор пользователя.

Если Вы разрабатываете бэкэнд мобильного приложения — то, скорее всего, нет. Для начала нужно определиться будете ли Вы использовать в своем приложении сессии при аутентификации/авторизации. Для использования сессий нужно активировать cookie-parser, session middleware, а также инициализировать сессию: Если это веб-приложение — то, скорее всего, да.

const app = express(); const sessionMiddleware = session(), secret, resave: true, rolling: true, saveUninitialized: false, cookie: { maxAge: 10 * 60 * 1000, httpOnly: false, },
}); app.use(cookieParser());
app.use(sessionMiddleware);
app.use(passport.initialize());
app.use(passport.session());

Тут нужно дать несколько важных пояснений. Если Вы не хотите, чтобы redis через пару лет работы съел всю оперативную память, нужно позаботиться о своевременном удалении данных сессии. За это отвечает параметр maxAge, который в равной степени устанавливает это значение и для cookie, и для значения сохраняемого в redis. Установка значений resave: true, rolling: true, продлевает срок действия заданным значением maxAge при каждом новом запросе (если это нужно). В противном случае сессия клиента будет периодически прерываться. И, наконец, параметр saveUninitialized: false не будет помещать в redis пустые сессии. Это позволяет разместить инициализацию сессий и passport.js на уровне приложения, не засоряя redis лишними данными. На уровне роутов инициализацию имеет смысл размещать только в том случае, если метод passport.initialize() необходимо вызывать с разными параметрами.

Если сессия не будет использоваться то инициализация значительно сократится:

app.use(passport.initialize());

Каждая стратегия имеет свои особенности конфигурирования. Далее нужно создать объект стратегии (так в терминологии passport.js называют способ аутентификации). Неизменным остается только то, что в конструктор стратегии передается callback-функция, которая формирует объект user, доступный как request.user для следующих в очереди middleware:

const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); })
);

Надо хорошо представлять себе, что если не используется сессия, это метод будет вызываться при каждом обращении к защищенному ресурсу, и запрос к базе данных (как в примере) существенно скажется на производительности приложения.

Каждая стратегия имеет имя по умолчанию. Далее нужно дать команду на использование стратегии. Но его можно задать и явно, что позволяет использовать одну стратегию с разными параметрами и логикой callback-функции:

passport.use('jwt', jwtStrategy);
passport.use('simple-jwt', simpleJwtStrategy);

Далее для защищаемого роута необходимо задать стратегию аутентификации и важный параметр session (по умолчанию равный true):

const authenticate = passport.authenticate('jwt', {session: false});
router.use('/hello', authenticate, (req, res) => { res.send('hello');
});

Если же сессия используется, то аутентификация происходит однократно, и для этого задается специальный роут, например login: Если сессия не используется, то защищать аутентификацией нужно все роуты с ограниченным доступом.

const authenticate = passport.authenticate('local', {session: true}); router.post('/login', authenticate, (req, res) => { res.send({}) ;
}); router.post('/logout', mustAuthenticated, (req, res) => { req.logOut(); res.send({});
});

При использовании сессии, на защищаемых роутах, как правило, используется очень лаконичное middleware (которое почему-то не включено в библиотеку passport.js):

function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next();
}

Итак, остался один последний момент — сериализация и десериализация объекта request.user в сессию/из сессии:

passport.serializeUser((user, done) => { done(null, user.id);
}); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; });
});

Сериализация будет выполнена ровно один раз сразу после аутентификации. Хочу еще раз подчеркнуть, что сериализация и десериализация работает только со стратегиями, для которых задан атрибут {session: true}. Десериализация будет выполняться при каждом запросе к защищенному роуту. Поэтому обновить данные сохраненные в сессии будет весьма проблематично, в связи с чем сохраняется только идентификатор пользователя (который не меняется). В связи с чем запросы к базе данных (как в примере) существенно влияют на производительность приложения.

Т.к. На этом можно было бы и закончить повествование. Однако, мне пришлось по требованию разработчиков фронтенда добавить в 401 ответ объект с описанием ошибки (по умолчанию это строка «Unauthorized»). кроме уже сказанного, на практике ничего другого не требуется. Для таких случаев нужно еще немного глубже залезть в ядро библиотеки, что не так уж и приятно. И это, как оказалось, не получается сделать просто. Небольшая проблема заключается в том, что этой функции не передается ни объект response, ни какaя-нибудь функция типа done()/next(), в связи с чем приходится самостоятельно преобразовывать ее в middleware: У метода passport.authenticate есть третий опциональный параметр: callback-функция с сигнатурой function(error, user, info).

route.post('/login', authenticate('jwt', {session: false}), (req, res) => { res.send({}) ;
}); function authenticate(strategy, options) { return function (req, res, next) { passport.authenticate(strategy, options, (error, user , info) => { if (error) { return next(error); } if (!user) { return next(new TranslatableError('unauthorised', HTTPStatus.UNAUTHORIZED)); } if (options.session) { return req.logIn(user, (err) => { if (err) { return next(err); } return next(); }); } req.user = user; next(); })(req, res, next); };
}

Полезные ссылки:

1) toon.io/understanding-passportjs-authentication-flow
2) habr.com/post/201206
3) habr.com/company/ruvds/blog/335434
4) habr.com/post/262979

apapacy@gmail.com
4 января 2019 года

Теги
Показать больше

Похожие статьи

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

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

Кнопка «Наверх»
Закрыть