Récemment, ils m'ont soutenu dans un projet sur express.js. En étudiant le code du projet, j'ai trouvé un travail un peu déroutant avec l'authentification / autorisation, qui était basée, comme 99,999% des cas, sur la bibliothèque passport.js. Ce code a fonctionné, et suivant le principe du "travail - ne touchez pas", je l'ai laissé tel quel. Quelques jours plus tard, on m'a confié la tâche d'ajouter deux autres stratégies d'autorisation. Et puis j'ai commencé à me rappeler que je faisais déjà un travail similaire et qu'il fallait plusieurs lignes de code. Après avoir parcouru la documentation sur passport.js, je n'ai presque pas bougé pour comprendre quoi et comment faire, car ils y ont examiné des cas où exactement une stratégie est utilisée, pour lesquels, pour chacun individuellement, des exemples sont donnés. Mais comment combiner plusieurs stratégies, pourquoi vous devez utiliser la méthode logIn () (qui est la même que login ()) - elle n'a toujours pas été clarifiée. Par conséquent, afin de comprendre maintenant, et de ne pas répéter la même recherche encore et encore, j'ai compilé ces notes pour moi-même.
Un peu d'histoire. Initialement, les applications Web utilisaient deux types d'authentification / autorisation: 1) de base et 2) à l'aide de sessions utilisant des cookies. Dans l'authentification / autorisation de base, un en-tête est envoyé dans chaque demande, et ainsi l'authentification du client est effectuée dans chaque demande. Lors de l'utilisation de sessions, l'authentification client n'est effectuée qu'une seule fois (les méthodes peuvent être très différentes, y compris Basic, ainsi que par nom et mot de passe, qui sont envoyées sous la forme, et des milliers d'autres méthodes appelées stratégies en termes de passport.js). L'essentiel est qu'après l'authentification, le client identifie l'identifiant de session dans le cookie (ou, dans certaines implémentations, les données de session), et l'identifiant utilisateur est stocké dans les données de session.
Vous devez d'abord décider si vous utiliserez des sessions dans votre application pour l'authentification / l'autorisation. Si vous développez un backend pour une application mobile, alors très probablement pas. S'il s'agit d'une application Web, alors très probablement oui. Pour utiliser des sessions, vous devez activer l'analyseur de cookies, le middleware de session et également initialiser la session:
const app = express(); const sessionMiddleware = session({ store: new RedisStore({client: redisClient}), 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());
Ici, vous devez donner quelques explications importantes. Si vous ne voulez pas que redis mange toute la RAM dans quelques années, vous devez vous occuper de la suppression en temps opportun des données de session. Le paramètre maxAge est responsable de cela, qui définit également cette valeur pour le cookie et la valeur stockée dans redis. La définition des valeurs resave: true, rolling: true, prolonge la période de validité avec la valeur maxAge spécifiée à chaque nouvelle demande (si nécessaire). Sinon, la session client sera interrompue périodiquement. Enfin, le paramètre saveUninitialized: false ne placera pas les sessions vides dans redis. Cela vous permet de placer l'initialisation des sessions et passport.js au niveau de l'application, sans obstruer les redis avec des données inutiles. Au niveau de l'itinéraire, il est logique de placer l'initialisation uniquement si la méthode passport.initialize () doit être appelée avec des paramètres différents.
Si la session n'est pas utilisée, l'initialisation sera considérablement réduite:
app.use(passport.initialize());
Ensuite, vous devez créer un objet de stratégie (comme passport.js appelle la méthode d'authentification dans la terminologie). Chaque stratégie a ses propres fonctionnalités de configuration. La seule chose qui reste inchangée est que la fonction de rappel est passée au constructeur de stratégie, qui forme l'objet utilisateur, accessible en tant que request.user pour les files d'attente de middleware suivantes:
const jwtStrategy = new JwtStrategy(params, (payload, done) => UserModel.findOne({where: {id: payload.userId}}) .then((user = null) => { done(null, user); }) .catch((error) => { done(error, null); }) );
Nous devons être conscients que si une session n'est pas utilisée, cette méthode sera appelée à chaque fois qu'une ressource protégée est accédée, et une requête de base de données (comme dans l'exemple) affectera considérablement les performances de l'application.
Ensuite, vous devez donner une commande pour utiliser la stratégie. Chaque stratégie a un nom par défaut. Mais il peut également être défini explicitement, ce qui permet d'utiliser une stratégie avec différents paramètres et la logique de la fonction de rappel:
passport.use('jwt', jwtStrategy); passport.use('simple-jwt', simpleJwtStrategy);
Ensuite, pour l'itinéraire protégé, vous devez définir une stratégie d'authentification et un paramètre de session important (la valeur par défaut est true):
const authenticate = passport.authenticate('jwt', {session: false}); router.use('/hello', authenticate, (req, res) => { res.send('hello'); });
Si la session n'est pas utilisée, l'authentification doit être protégée par toutes les routes d'accès restreint. Si la session est utilisée, l'authentification a lieu une fois, et pour cela une route spéciale est définie, par exemple la connexion:
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({}); });
Lors de l'utilisation d'une session, sur des routes protégées, en règle générale, un middleware très concis (qui, pour une raison quelconque, n'est pas inclus dans la bibliothèque passport.js) est utilisé:
function mustAuthenticated(req, res, next) { if (!req.isAuthenticated()) { return res.status(HTTPStatus.UNAUTHORIZED).send({}); } next(); }
Il y a donc eu un dernier moment: la sérialisation et la désérialisation de l'objet request.user vers / depuis la session:
passport.serializeUser((user, done) => { done(null, user.id); }); passport.deserializeUser((id, done) => { UserModel.findOne({where: {id}}).then((user) => { done(null, user); return null; }); });
Je tiens à souligner une fois de plus que la sérialisation et la désérialisation ne fonctionnent qu'avec des stratégies pour lesquelles l'attribut {session: true} est défini. La sérialisation sera effectuée exactement une fois immédiatement après l'authentification. Par conséquent, la mise à jour des données stockées dans la session sera très problématique, en rapport avec laquelle seul l'ID utilisateur (qui ne change pas) est enregistré. La désérialisation sera effectuée à chaque demande vers un itinéraire sécurisé. À cet égard, les requêtes de base de données (comme dans l'exemple) affectent considérablement les performances de l'application.
Remarque. Si vous utilisez plusieurs stratégies en même temps, le même code de sérialisation / désérialisation fonctionnera pour toutes ces stratégies. Pour prendre en compte la stratégie par laquelle l'authentification a été effectuée, par exemple, vous pouvez inclure un attribut de stratégie dans l'objet utilisateur. Cela n'a également aucun sens d'appeler la méthode initialize () plusieurs fois avec des valeurs différentes. Il sera toujours réécrit avec les valeurs du dernier appel.
Cela pourrait être la fin de l'histoire. Parce que en plus de ce qui a été dit, dans la pratique, rien d'autre n'est requis. Cependant, à la demande des développeurs frontaux, j'ai dû ajouter un objet avec une description de l'erreur à la réponse 401 (par défaut, il s'agit de la ligne non autorisée). Et cela s'est avéré impossible à faire simplement. Pour de tels cas, vous devez approfondir un peu le cœur de la bibliothèque, ce qui n'est pas si agréable. La méthode passport.authenticate a un troisième paramètre facultatif: une fonction de rappel avec la fonction de signature (erreur, utilisateur, info). Le petit problème est que ni l'objet de réponse ni aucune fonction comme done () / next () n'est passé à cette fonction, et vous devez donc le convertir vous-même en middleware:
route.post('/hello', 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); }; }
Liens utiles:
1)
toon.io/understanding-passportjs-authentication-flow2)
habr.com/post/2012063)
habr.com/company/ruvds/blog/3354344)
habr.com/post/2629795)
habr.com/company/Voximplant/blog/3231606)
habr.com/company/dataart/blog/2628177)
tools.ietf.org/html/draft-ietf-oauth-pop-architecture-088)
oauth.net/articles/authenticationapapacy@gmail.com
4 janvier 2019