How to handle refresh tokens
The point of having access tokens is that they can be used without checking for invalidation. You can have 10000 frontend servers users can access with the token without the need to ever ask some database if it is invalid. But after some time, the token expires. The user needs a new access token, sends her refresh token and this refresh token is checked in some database. You never need to check for expired access tokens or have any state, but limit abuse to the lifetime of the token.
If you don't have the requirement to accept the tokens without checking expiration in a database, you don't need the two different tokens. You can just use the refresh token for each access.
Example workflow would be:
User logs in, gets access and refresh token. Access token lifetime 15min, refresh token 5 days.
User accesses the service using the access token. Service only checks signature and lifetime. No database connection.
- User logs out, the refresh token is marked expired in the database
- User accesses the service using the access token, this still works
- 15min pass. Access token expires, user requests a new access token using the refresh token still within its lifetime. The service checks the database and finds the token is expired. User can't get a new access token.
The whole system is a trade-off between the time it takes to invalidate sessions and the amount of connections to a shared/synchronized data source required.
You can replace the refresh token on each refresh, but remember that you need to store all expired refresh tokens until their lifetime is over.
From a security perspective it makes sense to create a new token, but it is a trade off between security and amount of data in your database.
In the worst case (no lifetime for refresh tokens, never do that) you now need to store one token every few minutes in the database for each user instead of one token for each user and you can never remove them again.
Refresh tokens are one of those technologies where the practice and the theory don't match, in my experience. In theory, you make a login request, and get back an access token (with a short lifetime) and a refresh token (which has either a long expiry period, no expiry, and can be used to get a new access token at any point). If the client tries to send an expired access token, and gets a rejection from the server, it can send the refresh token, get a new access token, then continue.
In this case, it is very clear that the refresh token is really powerful, and needs to be stored carefully (e.g. in credential storage on a mobile device, rather than a browser cookie, or in a carefully secured server side store). However, the specifications do allow for a refresh token to be invalidated by the authentication server.
In practice, therefore, it's not uncommon for a refresh token to be issued on login, and invalidated on logout or after a period of inactivity. In this case, it can be part of the session data on the server, and hence automatically linked to the current user. Any other user would only be able to use the refresh token from within the authenticated session, meaning that any logic linking it to a specific user would also fail.
From the code snippet above, this is pretty much what you have: the refresh token is generated each login, rather than being retrieved from storage after being generated at account creation, for instance. The only part which is missing is in how the refresh token is queried - it is implied that the refresh token is considered sufficient to get an access token, rather than the combination of an authenticated session and a refresh token. This is, as you suspect, insecure.
The refresh token provides authorization to obtain a new access token, but does not authenticate that the person requesting the access token is the one who should have access. You need to provide the authentication step before accepting the authorization, and ensure this is used every time the refresh token is used - an open session may be sufficient.
You can choose to replace the refresh token on every new access token. This has a side benefit of breaking fairly obviously if multiple users attempt to use the refresh token (the second one fails) hence limiting concurrent use. Avoid issuing new refresh tokens without expiring the old one, however, since this increases the potential for token compromise. It is probably of limited benefit in the case where the refresh token expires with the session (assuming a short session lifetime), but can help with longer sessions (e.g. "remember me" functions).