mikenicholson / passport-jwt

Passport authentication using JSON Web Tokens
MIT License
1.96k stars 213 forks source link

Multiple audiences in same route #213

Closed andresilva-cc closed 4 years ago

andresilva-cc commented 4 years ago

Hello everyone, Imagine this scenario:

In my API there are 2 types of users, those who access from a mobile application (who I'm gonna call app), and those who access from a web application (who I'm gonna call web). They have different responsibilities and rights so that why I need to separate their audience (in the JWT).

Now, there's one specific route where both can access it. Fine, in my callback of JWTStrategy I could see what's the type of user by the audience string and then do the right query. But, how can I specify in my route (maybe with a parameter) what's the audience (or audiences) I'm targeting and pass it to inside the strategy callback?

andresilva-cc commented 4 years ago

Well, I could solve it in an ugly way, check it out:

import Passport from 'passport';

/**
 * Passport JWT Strategy Middleware
 *
 * @export
 * @param {(string|string[])} audience Audience to verify against
 * @returns {function} Handler function
 */
export default function jwtStrategyMiddleware(audience) {
  /**
   * Handler function
   *
   * @param {Request} req Request
   * @param {Response} res Response
   * @param {NextFunction} next Next function
   * @returns {Response} Response
   */
  return (req, res, next) => {
    // If single audience
    if (typeof audience === 'string') {
      // If app audience
      if (audience === 'app') {
        return Passport.authenticate('studentJWT', { session: false })(req, res, next);
      }

      // If web audience
      if (audience === 'web') {
        return Passport.authenticate('administratorJWT', { session: false })(req, res, next);
      }
    }

    // Else, if multiple audiences
    // First, try to authenticate with administratorJWT
    return Passport.authenticate('administratorJWT', { session: false }, (_info, _user, err) => {
      // If failed, try to authenticate with studentJWT
      if (err) {
        return Passport.authenticate('studentJWT', { session: false })(req, res, next);
      }

      // If successful, call next handler
      return next();
    })(req, res, next);
  };
}

Then you use it like this:

// If single audience
app.get('/example', jwtStrategy('web'), ExampleController.example);

// If multiple audiences
app.get('/example', jwtStrategy(['app', 'web']), ExampleController.example);

If anyone knows a more elegant way, please let me know.

andresilva-cc commented 4 years ago

Ok, I discovered that Passport.authenticate also accepts an array of strings (strategy names). For some reason, I didn't find this yesterday.

So now my middleware is much more simple and elegant, thank's to that:

import Passport from 'passport';

/**
 * Map audience to strategy name
 *
 * @param {(string|string[])} audience Audience to map
 * @returns {(string|string[])} Strategy or strategies names
 */
function mapAudience(audience) {
  const map = {
    app: 'studentJWT',
    web: 'administratorJWT',
  };

  if (typeof audience === 'string') {
    return map[audience];
  }

  return audience.map((v) => map[v]);
}

/**
 * Passport JWT Strategy Middleware
 *
 * @export
 * @param {(string|string[])} audience Audience to verify against
 * @returns {function} Handler function
 */
export default function jwtStrategyMiddleware(audience) {
  /**
   * Handler function
   *
   * @param {Request} req Request
   * @param {Response} res Response
   * @param {NextFunction} next Next function
   * @returns {Response} Response
   */
  return (req, res, next) => {
    Passport.authenticate(mapAudience(audience), { session: false })(req, res, next);
  };
}