26

Currently im trying to gather knowledge on how to implement an auth system (a login) . And during my research i've tried to implement a JWT based solution in my backend.

I have an express server which allows me to register an user , storing its password (encrypted) and its email.

After that on login, it generates an access token (short lived, 5min) , in order to access protected routes, and a refresh token (long lived, 7 days), in order to generate new access tokens once the previous expire.

In my current implementation, im storing the refresh token in my database, so I can use it every time I want to generate a new access token.

But is that secure? As far as I understand, storing an access token in my database is dangerous, so its better to create a short lived cookie stored one. But... the refresh token? As far as i understand it would be dangerous since it basically allows to generate new access tokens, so I dont see the point of why not simply storing a long lived access token in my database, an generating a new one in every login.

Whats is the refresh token for then?

Since im following some tutorials in order to achieve this, this is how my refresh_token route looks

 //get a new access token with a refresh token
app.post('/refresh_token', (req, res) => {
    const token = req.cookies.refreshtoken
    //if no token in request
    if(!token) return res.send({accesstoken : ''});
    //if we have a token we verify it
    let payload = null;
    try{
        payload = verify(token, process.env.REFRESH_TOKEN_SECRET);
    }catch(err){
        return res.send({accesstoken: ''});
    }
    //if token is valid check if user exist
    const user = fakeDB.find(user => user.id === payload.userId)
    if(!user) return res.send({ accesstoken: ''});
    //if user exists check if refreshtoken exist on user

    //Is this really necessary? <-------------------------------------------

     if(user.refreshtoken !== token){
         return res.send({accesstoken: ''}) 
     }


    //if token exist create a new Refresh and Accestoken
    const accesstoken = createAccessToken(user.id);
    const refreshtoken =  createRefreshToken(user.id);
    user.refreshtoken = refreshtoken;
    //send new refreshtoken and accesstoken
    sendRefreshToken(res, refreshtoken);
    return res.send({accesstoken});
})

The arrow comment is where I have my doubts, ok its returning an empty access token if my database table user (its a mock database so an array so far) , doesnt have stored a refresh token. But why would you do that? Is that used to not let arbitrary users generate access tokens? As far as I understand thats the only reason of why would I do that.

But again then, isnt it dangerous to store in a database? WHy not simply store the access token then make it a long lived token, and generate a new one in every login?

Is there a method to do it simplier than with jwt?

mouchin777
  • 1,428
  • 1
  • 31
  • 59
  • 2
    see https://stackoverflow.com/a/39003201/4855817 and https://stackoverflow.com/a/57503520/5175709 – Alex Dec 28 '19 at 14:37

2 Answers2

42

Why access tokens should be short-lived: if you want a decentralised auth flow (authentication service signs a token, other services can verify if it's valid using an asymmetric public key), you want that token to be short-lived because it cannot be blacklisted in case it's stolen (an attacker can use it until it expires). You can of course blacklist access tokens using i.e. Redis, but your auth flow won't be decentralised anymore. All services will have to validate that token using the asymmetric public key AND check if it's blacklisted or not (better just ask authentication service if it's valid or not).

This is how I would go about it:

  • 5 minute access token as JWT (self-contained, don't need to store it anywhere).

  • 7 day refresh token for one-time usage: generate random secret (don't need to sign it/encrypt it), store it in Redis with a 7 day TTL (or MySQL with a valid_until timestamp). On /refresh_token validate the provided token (check if it's in Redis/MySQL) and delete it. Generate a new access and refresh token pair. (I like to rotate refresh tokens as well, it makes it a bit more secure: it's probably already rotated=invalid if stolen)

This way the auth flow stays decentralised and refresh tokens can be revoked if they are stolen.

h3yduck
  • 1,571
  • 14
  • 14
  • 3
    Why would you not encrypt the refresh token? If the db gets "leaked" then the attacker would get a plain text string that he can use to get access (and be able to refresh forever). – cottton Feb 04 '22 at 02:11
  • @cotton There is a pretty good explanation here: https://github.com/doorkeeper-gem/doorkeeper/issues/320#issuecomment-28909509. IF the database (DB) gets leaked, then just truncate the table that stores the tokens so that attackers cannot login to the live system that is currently in production. This will require all existing users to re-authenticate. Other than that, there's nothing you can do about a leaked DB. Chances are that whoever was able to compromise the system to the point of leaking it then they probably had access to sensitive settings and data stores - keys could be leaked too. – J Weezy Aug 15 '22 at 20:08
  • It's not necessarily true that you cannot black list a a stolen token. Simply place the issue date inside the token, and have a lookup table for sessions that are expired by datetime. Storing tokens in a DB can lead to compromised data in the case of a data leak. – THE AMAZING Feb 19 '23 at 15:38
5

Refresh tokens should be encrypted in storage. The OAuth 2.0 Threat Model and Security Considerations RFC goes into this:

4.5.2. Threat: Obtaining Refresh Token from Authorization Server Database

This threat is applicable if the authorization server stores refresh tokens as handles in a database. An attacker may obtain refresh tokens from the authorization server's database by gaining access to the database or launching a SQL injection attack.

Impact: Disclosure of all refresh tokens.

Countermeasures:

  • Enforce credential storage protection best practices (Section 5.1.4.1).

  • Bind token to client id, if the attacker cannot obtain the required id and secret (Section 5.1.5.8).

And then the referenced section 5.1.4.1.3:

5.1.4.1.3. No Cleartext Storage of Credentials

The authorization server should not store credentials in clear text. Typical approaches are to store hashes instead or to encrypt credentials. If the credential lacks a reasonable entropy level (because it is a user password), an additional salt will harden the storage to make offline dictionary attacks more difficult.

Note: Some authentication protocols require the authorization server to have access to the secret in the clear. Those protocols cannot be implemented if the server only has access to hashes. Credentials should be strongly encrypted in those cases.

ZachB
  • 13,051
  • 4
  • 61
  • 89
  • You are the first person other than myself that has brought up 4.5.2 I would agree that encrypting the tokens would be suffice any concerns i would have. in terms of password hashing i always prefer to use CBC AES 256 instead. – THE AMAZING Feb 19 '23 at 15:43