3

I followed a tutorial to add login and registration to my Node.js app using JWT token and I'm having a hard time logging in and redirecting to my 'logged in' admin page. User registration works great, but the login portion I can't figure out.

This is the tutorial I was following: https://medium.freecodecamp.org/learn-how-to-handle-authentication-with-node-using-passport-js-4a56ed18e81e

My code for login looks like this:

router.post('/login', auth.optional, (req, res, next) => {
console.log(req.body);

var user = {
    email: req.body.email,
    password: req.body.password
}

if (!user.email) {
  return res.status(422).json({
    errors: {
      email: 'is required',
    },
  });
}

if (!user.password) {
  return res.status(422).json({
    errors: {
      password: 'is required',
    },
  });
}

return passport.authenticate('local', { session: false }, (err, passportUser, info) => {

    if (err) {
        return next(err);
    }

    if (passportUser) {
        const user = passportUser;
        user.token = passportUser.generateJWT();
        console.log("TOKEN: " + user.token);

        res.setHeader('Authorization', 'Token ' + user.token);

        return res.json({ user: user.toAuthJSON() });
    }

    return res.status(400).json({
        errors: {
            message: info,
        },
    });

    })(req, res, next);
});

My '/admin' "logged in" route looks like this:

router.get("/admin", auth.required, function(req, res) {
    res.render('admin', {
        user : req.user // get the user out of session and pass to template
    });
});

I'm not sure how I can redirect to my '/admin' route while also passing the token because currently I am seeing the following error after logging in. Makes sense since I am not passing the token to the '/admin' route...but how do I do that? :)

UnauthorizedError: No authorization token was found at middleware

Thanks in advance for the help!

EDIT:

Still can't figure this out and don't really understand how this flow is supposed to work...where do the headers need to be set to the token and how do I redirect to my admin page once the login is successful.

Here is my middleware code if this helps:

const getTokenFromHeaders = (req) => {

    console.log("REQ: " + JSON.stringify(req.headers));

    const { headers: { authorization } } = req;

    if(authorization && authorization.split(' ')[0] === 'Token') {
        return authorization.split(' ')[1];
    }

    return null;
};

const auth = {
    required: jwt({
        secret: 'secret',
        userProperty: 'payload',
        getToken: getTokenFromHeaders,
    }),

    optional: jwt({
        secret: 'secret',
        userProperty: 'payload',
        getToken: getTokenFromHeaders,
        credentialsRequired: false,
    }),
};
user2573690
  • 5,493
  • 9
  • 43
  • 61
  • Any reason why you are not returning the token to the user and letting the user make a request to /admin with the token? – s_a94248 Apr 05 '19 at 09:48
  • @singh_arpit hmm currently the user has to enter their credentials, click 'login' and then they are sent to the /admin page if the credentials are correct...if I return the token then the user would need to click login again or how would that flow work? – user2573690 Apr 09 '19 at 00:58
  • So, the flow is usually. Login => Backend returns the token to the client (in the form of cookies or response data) => The frontend uses this token and sets it as a default header in every api call => A middleware in the backend is setup before the secured api's which validate the token. – varun agarwal Apr 09 '19 at 06:20
  • I have updated the answer to accommodate the desired behavior, where user will be redirected to admin page. – aitchkhan Apr 15 '19 at 19:07
  • I missed the bounty tho :P – aitchkhan Apr 19 '19 at 19:42

2 Answers2

1

I'm somewhat new to this as well, but I've got it working as follows.

In your server.js file:

const passport = require("passport");
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
app.use(passport.initialize());
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = Keys.secretOrKey;
passport.use(
  new JwtStrategy(opts, (jwt_payload, done) => {
    // somefunction looks up the id in jwt payload and 
    // supplies passport the authenticated user via the "Done" function
    somefunction.user(jwt_payload.id)
      .then(user => {
        if (user) {
          return done(null, user);
        }
        return done(null, false);
      });
  })
);

In your API definitions

const jwt = require("jsonwebtoken");

router.post("/login", (req, res) => {
  const { userInfo } = req.body;

  // userInfo has username and password in it
  // anotherFuction validates the user id and password combo
  anotherFunction(userInfo.id, userInfo.password)
    .then(isAuthenticated => {
      if (isAuthenticated) {

        const payload = {
          id: user.sAMAccountName,
          firstname: user.givenName,
          lastname: user.sn
        };

        // Sign Token with the payload
        jwt.sign(
          payload,
          Keys.secretOrKey,
          { expiresIn: 3600 },
          (err, token) => {
            res.json({
              success: true,
              token: "Bearer " + token
            });
          }
        );
      } else {
        // don't mind the statuses ^_^'
        return res.status(401).json({ error: "Login failed." });
      }
    })
    .catch(err => {
      return res.status(400).json(err);
    });
});

After calling the API you want to set the auth token. The following lets you delete the token if nothing is passed in, effectively "Logging out".

const setAuthToken = token => {
  if (token) {
    // Apply to every request
    axios.defaults.headers.common["Authorization"] = token;
  } else {
    // Delete Auth Header
    delete axios.defaults.headers.common["Authorization"];
  }
};

If you're trying to use it in the front end, you need to use jwt_decode to pull the values from the token and set it however you deem necessary. If using redux to store login data it should look something like this. As I feel that the discussion of using localstorage for jwtToken is outside of the scope of this, just know would need to check for the token.

if (localStorage.jwtToken) {
  setAuthToken(localStorage.jwtToken);
  const decoded = jwt_decode(localStorage.jwtToken);
  store.dispatch({
    type: USER_LOGIN,
    payload: decoded
  });
}

Hope this helped.

From one beginner in JWT to another. Good luck.

Likwidsage
  • 81
  • 2
  • 5
1

Your code does not have a problem. You seem to be confused with the login flow from server to client (Frontend/Web).

Let's first have a look the RESTFUL way of doing it. The article also refers to the same flow.

The RESTFUL API flow looks like this:

User requests for login: POST: /api/v1/auth/login with username and password in request body. If successful, user is returned with basic inforamtion and token. If not, user is returned a 401 (Unauthorized) status code. The login flow ends here.

The token provided earlier to the user is used to make subsequent calls to the backend, which a user can use to perform different operations on the sustem. In essence, it is the client which requests server for subsequent actions with the token provided in the login request.

So for your case, user after receiving the token should make a request for retrieving admin information from the backend.

But, I am assuming you are rendering views from your server-side and you want to render the admin view once the user is successfully logged in, and that's pretty straight forward.

Instead of your res.json() after successful login. You need to use res.render().

res.render('admin', {
    user: user.toAuthJSON() // assuming your user contains the token already
})

Edit:

Since res.render() does not change the url in the browser. For that, you need to use res.redirect(). But the problem is, you can not send context in res.redirect().

To achieve that, you will need to pass in the user token as query paramter. See here.

TL;DR

// assuming you are using Node v7+
const querystring = require('querystring');
const query = querystring.stringify({
         token: user.token,
});
const adminRoute = '/admin?' + query;
res.redirect(adminRoute)

And in your admin route, you need to slightly modify the code.

  1. Verify the token belongs to a real user and get user information out of the token.
  2. Render the admin template with user information retrieved from step 1.
router.get("/admin", function(req, res) {
    // verify the token
    const token = req.query.token;
    const user = null;
    jwt.verify(token, 'secret', function (err, decoded) {
    if (err) {
      res.status(401).send('Unauthorized user')
    }
    // decoded contains user
    user = decoded.user  
  });

    res.render('admin', {
        user : user
    });
});

aitchkhan
  • 1,842
  • 1
  • 18
  • 38