Is it secure to store a refresh token in the database? (To issue new access tokens for login purposes). Or is there a method to do it easier?

mouchin777 picture mouchin777 · Dec 28, 2019 · Viewed 7.4k times · Source

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?

Answer

h3yduck picture h3yduck · Dec 28, 2019

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.