Once again about passport.js

    Recently I was transferred to support a project on express.js. When studying the project code, I found a little confusing work with authentication / authorization which was based, like 99.999% of cases, on the passport.js library. This code worked, and following the principle “works - don't touch”, I left it as it is. When in a couple of days I was asked to add two more authorization strategies. And then I started to remember that I was already doing a similar job, and it took a few lines of code. Having looked through the documentation on passport.js, I almost did not budge in understanding what and how to do, because there were considered cases where exactly one strategy is used, for which, for each separately, and examples are given. But how to combine several strategies, why do we need to use the logIn () method (which is the same, that login ()) is still not clear. Therefore, to understand now, and not to repeat the same search again and again, I made these notes for myself.

    A bit of history. Initially, web applications used two types of authentication / authorization: 1) Basic and 2) using cookie-based sessions. In Basic authentication / authorization, a certain header is transmitted in each request, and thus, each client is authenticated in each request. When using sessions, client authentication is carried out only once (methods can be very different including Basic, and also by name and password that are sent in the form, and thousands of other methods that are called strategies in terms of passport.js). The main thing is that after passing through authentication, the session ID (or in some implementations session data) is stored in the cookie in the cookie, and the user ID is stored in the session data.

    First, you need to decide whether you will use authentication / authorization sessions in your application. If you are developing a backend mobile application - then, most likely, no. If this is a web application, then most likely, yes. To use sessions, you need to activate cookie-parser, session middleware, and also initialize the 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());
    

    Here it is necessary to give some important explanations. If you do not want redis to eat all the RAM after a couple of years of work, you need to take care of deleting session data in a timely manner. The maxAge parameter is responsible for this, which equally sets this value for both the cookie and the value stored in redis. Setting the resave value: true, rolling: true, extends the validity period by the specified maxAge value for each new request (if necessary). Otherwise, the client session will be interrupted periodically. And finally, the saveUninitialized parameter: false will not put empty sessions in redis. This allows you to put session initialization and passport.js at the application level, without clogging redis with unnecessary data. At the level of routes, initialization makes sense to place only if the passport method.

    If the session is not used, initialization will be significantly reduced:

    app.use(passport.initialize());
    


    Next, you need to create an object of the strategy (this is how the authentication method is called in the terminology of passport.js). Each strategy has its own configuration features. What remains unchanged is that the callback function is passed to the strategy constructor, which forms the user object, available as request.user for the middleware in the queue:

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


    You need to be well aware that if a session is not used, this method will be called each time a protected resource is accessed, and a database query (as in the example) will significantly affect the performance of the application.

    Next you need to give a command to use the strategy. Each strategy has a default name. But you can set it explicitly, which allows you to use the same strategy with different parameters and logic callback functions:

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


    Next, for the protected route, you need to set an authentication strategy and an important session parameter (by default, equal to true):

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


    If the session is not used, then all authenticated routes should be protected with authentication. If a session is used, then authentication occurs once, and for this a special route is set, for example 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({});
    });
    


    When using a session, on protected routs, as a rule, very laconic middleware is used (which for some reason is not included in the passport.js library):

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


    So, there is only one last thing left - serialization and deserialization of the request.user object into / from a session:

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


    I want to emphasize once again that serialization and deserialization work only with strategies for which the {session: true} attribute is specified. Serialization will be performed exactly once immediately after authentication. Therefore, updating the data stored in the session will be very problematic, and therefore only the user ID (which does not change) is saved. Deserialization will be performed at each request to a protected route. In this connection, requests to the database (as in the example) significantly affect the performance of the application.

    Comment. If you use several strategies at the same time, the same serialization / deserialization code will work for all of these strategies. To account for the strategy for which authentication has been passed, for example, you can include a sign of the strategy in the user object. It also does not make sense to call the initialize () method several times with different values. It will still be rewritten by the values ​​from the last call.

    On this one could finish the story. Since except for what has already been said, in practice nothing else is required. However, I had to, at the request of the front-end developers, add an object with a description of the error to 401 responses (by default, this is the “Unauthorized” line). And this, as it turned out, cannot be done easily. For such cases, you need to get into the core of the library a little deeper, which is not so pleasant. The passport.authenticate method has a third optional parameter: a callback function with a function signature (error, user, info). A minor problem is that neither the response object, nor any function of the type done () / next () is passed to this function, and therefore you have to convert it yourself into middleware:

    route.post('/hello', authenticate('jwt', {session: false}),  (req, res) => {
      res.send({}) ;
    });
    functionauthenticate(strategy, options) {
      returnfunction (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);
      };
    }
    


    Useful links:

    1) toon.io/understanding-passportjs-authentication-flow
    2) habr.com/post/201206
    3) habr.com/company/ruvds/blog/335434
    4) habr.com/post/262979
    5) habr .com / company / Voximplant / blog / 323160
    6) habr.com/company/dataart/blog/262817
    7) tools.ietf.org/html/draft-ietf-oauth-pop-architecture-08
    8) oauth.net/ articles / authentication

    apapacy@gmail.com
    January 4, 2019

    Also popular now: