Главная » Хабрахабр » Еще раз о passport.js

Еще раз о 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 года


Оставить комментарий

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

*

x

Ещё Hi-Tech Интересное!

В России приступили к тестированию отечественного нейроинтерфейса «Нейрочат»

Эта система предназначена для пациентов с ограниченными физическими способностями. В конце прошлого года компания Neurotrand разработала вместе с партнерами программно-аппаратный комплекс с нейрогарнитурой. Это могут быть пациенты клиник, перенесшие инсульт, военные с тяжелыми ранениями, люди, получившие травму на производстве. Она ...

Зрители не могут отличить нативную картинку 4K от интерполяции

Такие выводы можно сделать из результатов российского исследования, проведённого холдингом «Ромир». Человеческого зрения недостаточно, чтобы отличить настоящее видео 4K от картинки, которую получили из изображения HDTV с помощью интерполяции. Опрошенным показывали на телеэкране фрагменты двух видеороликов и спрашивали о восприятии ...