Question

Here's a breakdown of the authentication flow:

  • user registers (email, password)
  • user logs in
  • if their login is valid, a token is generated on the server side with the following code:

    require('crypto').randomBytes(48, function(ex, buf) {
        var token = buf.toString('hex');
        // generates a token, such as....
        //  9b50ea46e80804bfe2ae01d0d1bb099c26887a65c92f61e47677c28ed40dbd4ef4c14f0dc58688ab4ec0df6b766ec90f
    });
    
  • that token is returned to the client side, and saved as a cookie

  • the users IP address that they logged in with is stored along with the token into the database
  • that token is stored in the database hashed, with the following code:

    var salt = bcrypt.genSaltSync(10);
    bcrypt.hash(token, salt, null, function(err, hash) {
        token = hash;
        token.save();
        // salts and hashes a token and saves that hashed token to the database
        // resulting hashed token will look like...
        // $2a$10$rGjMO6bWb/R4/yAAEV8Nx.7Fr6bS.AmMS0vRYB7p5umTpfpjMOfAC
    });
    
  • on all future requests FROM the client TO the server, an "auth_token" header is automatically attached to all requests of all types, containing the unhashed token that was given to the client earlier which was saved to a cookie

  • when a request comes into the server, it checks if the "auth_token" header is attached
  • if the "auth_token" header DOESN'T exist, it denies their request for data from the API
  • if the "auth_token" header DOES exist, it...
    • checks if the token is valid (exists in the database by doing a bcrypt.compare against the tokens in the database until a match is found) and belonging to the current user who is interacting with the app (the user id sending the request matches the user id attached to that token in the DB)
    • checks if the IP address requesting the data matches the one attached to that token in the database
    • checks if the token has expired (on the serverside, not the clientside cookies)
  • If all of the above tests pass, it gives the user the data they're looking for... if it failed any of them, it gives them a 403 forbidden.

This is my first token authentication system that I've made from scratch for learning purposes.

Thoughts / criticisms / questions welcome! If I missed explaining any key part of this setup, just ask away and I'll clarify.

The only way that I can see somebody being able to abuse this would be:

  • Get access to someones computer
  • Get their token from their cookie
  • Send requests to the API, spoofing that users IP, using their token, and within the timeframe before the token expires
  • Data granted

But in order to do that, they'd have to basically get access to that persons computer in order to get the cookie, and if they've got access to that persons computer, they've already basically got unlimited access to the data ANYWAY because odds are that person is still logged into the website and/or that person has their password and username auto-filling on the website, etc.

Was it helpful?

Solution

There are two things that pop out at me:

  • Never ever ever use a synchronous method in real code. (genSaltSync) Cryptographic methods are computationally expensive; doing them on the JS thread is a recipe for disaster. A very small number of concurrent login attempts will grid your server to a halt.

  • You are treating the user's IP address as a constant, but this is not a valid assertion. Between DHCP, mobile devices that change networks frequently, VPNs, and proxy servers, you have no way of knowing whether a user's next request will come from the same IP.

    The support nightmare you'll have from users who get randomly denied access is (IMO) not worth the speculative security gain. As long as you've properly configured SSL and set your cookies Secure and HttpOnly, the risk of a stolen token is small.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top