nickredmark / ooth

User identity/authentication/accounts management microservice for node.js
https://nmaro.github.io/ooth/
MIT License
605 stars 65 forks source link

Refining JWT use: removing non-expiry vulnerability, enabling sessionless authentication, size considerations #52

Closed heysailor closed 6 years ago

heysailor commented 6 years ago

Thanks for a great looking project. Reading your docs, it looks like you're oriented towards establishing a session with an API using a cookie, whether standalone or integrated, after obtaining a JWT via ooth to show authentication status.

However:

Non expiring JWT vulnerability

Looking at the ooth module, a non-expiring JWT is issued in exchange for a valid login. This means you can never log out a user - if they have the JWT, they can always pass it to the API for use, and it will be valid. Unless the API tracks a JWT blacklist, it will be unable to block malicious users. Tracking a blacklist means database calls, and the point of JWTs is to prove authentication without database access.

ooth/src/index.js:319:

  getToken(user) {
    return sign({ user }, this.sharedSecret);
  }

I think it would be preferable to use short lived JWTs. This ensures that creating a session with the API can only be done for a limited amount of time. The session cookie can be as long as the API wishes.

The JWT expiry can be set as an exp field as observed by Passport.

  getToken(user) {
    return sign({ 
       user,
       exp: ...some time in the future
     }, this.sharedSecret);
  }

Enabling sessionless JWT authentication with refresh tokens

There's a significant use case of authenticating every API request with a JWT, instead of a cookie based session. This is the typical authentication method used in mobile apps, eg React Native. For this use case, if JWTs are to have an expiry, a method must be provided to obtain new JWTs without logging in every time.

A refresh token is typically used: https://www.jaygould.co.uk/dev/2017/07/18/jwt-token-refresh-redux-react-native.html

In short, using the refresh token causes a database check to ensure:

The UI experience for the user is the same, but with more security:

It seems this would be handy for many and will increase security.

JWT size/single concern contents

A disadvantage to using the ooth JWT token for authenticating all requests is that the ooth use does not quite adhere to the JWT spec....they are mean to be compact.

Ooth places the user profile uniqueFields information into the JWT. Okayish if the JWT is just used once to send to an API to establish a cookie-based session. But sending a large JWT token for every request becomes a problem when the user profile is large - eg Facebook data.

For instance, when logging in with the local strategy, the JWT token provided on login contains:

{
    user: {
         _id: <user id>,
        local: {
            username: undefined,
            email: <user email>,
           verified: undefined
        }
    }
}

The JWT is being used to transfer user data, rather than the single concern of authentication status. This mix of concerns is reflected in the response body, which has both a user field, containing the same information as encoded in the token field.

The minimal, single-concern JWT token implementation would be to only place the user _id (and perhaps as a future feature authorisation data such as roles or similar) into the JWT. The fact of having a valid JWT with that user id means that authentication has occurred. If the API needs more user data, it can always access the database.

This is how Meteor approaches tokens - a resumeToken isn't even a JWT; it's simply a random string which optionally expires, see line 589. Holding the resumeToken signifies only that the user was authenticated; the token itself contains no data.

Minimal JWT:

{
    _id: <user id>,
    exp: <UNIX time in future>,
    iat: <UNIX time of issue>
}

Naturally, it's still very handy/essential for each strategy to administer profile fields. They can still be provided in the user field of the response body, or accessed by an API from the database.

Happy to potter along with a PR(s) to address these issues, but would be great to get thoughts before starting.

aaronmgdr commented 6 years ago

I believe this is what the standalone option does

heysailor commented 6 years ago

(Extensively!) edited initial comment to make it clearer. As far as I can see, standalone issues a single non-expiring JWT in exchange for correct login details.

nickredmark commented 6 years ago

Hi @heysailor thank you for your extensive ticket. Sorry I didn't get back to you as of yet, was completely off the grid for 2 weeks.

nickredmark commented 6 years ago

Thank you for your help, this is now released.