Reputation: 1463
I want to keep tokens like this (payload):
access_token
{
"user": "john_doe",
"iat": 1444262543,
"exp": 1444262563, // 15 minutes
"type": "access"
}
refresh_token
{
"user": "john_doe",
"iat": 1444262543,
"exp": 1444262563, // 24 hours
"type": "refresh"
}
As you can see access_token and refresh_token are almost identical except of lifetime and type.
Is this a problem? Can it be security hole when using Refresh token this way?
PS: The reason behind this is because I don't want to store refresh token in a storage (DB/Redis).
Upvotes: 0
Views: 675
Reputation: 48
From security perspective, self contained refresh tokens are vulnerable.
The reason we have refresh token is to extend the validity of the access token. In other words, we hit an Authorization server (or have a different flow altogether for authorization), verify it using some public key and get the access token from the service (created using private key).
A refresh token HAS to be stored on the server side. We shouldn't leverage the "self-contained" property of JWT for a refresh token. Doing so leaves us with no way to revoke refresh tokens other than changing our private key.
Example: Suppose i'm logged into your application from my cellphone and it's lost. The application contains the refresh token right, and that contains all my information. If refresh_token is not stored at the backend, we do not have any way to invalidate that session. Now the private key which creates the token needs to be changed, which is not good.
The higher level steps to create access_token from refresh_token could be something like this:
Let's lay the groundwork first:
The tokens table in the database could contain the following fields:
user_id<uuid>: id of the user
access_token<text>: access token
access_token_expiry<date>: access token expiring timestamp
refresh_token<text>: refresh token (sha)
refresh_token_expiry<date>: refresh token expiring timestamp
refresh_count: number of times the refresh token has been used to access access_token (just in case if you need this field)
Note that we should store the hash of the refresh token in the database.
Also, one user could have multiple sessions, say they're logged into our application from different browsers on the same device / from different devices. To cater to this requirement, we need to create one login_sessions table
id<BIGINT>: primary key for the table
user_id<uuid>: id of the user who logged in
map_id<uuid>: This will be the key which maps one access token to its refresh token counterpart. Both tokens of the pair will contain this id in their body.
status<String>: could be ACTIVE|INACTIVE(logged out, INACTIVE user will not be allowed to get access_token from the refresh_token)
created_on<Date>: timestamp for record created on
modified_on<Date>: timestamp for record modified on
refresh_token body:
{
"iss": "ABC Service", (The Issuer of the token)
"sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
"aud": "UI", (audience who will use the token)
"typ": "refresh", (type)
"iat": 1599326501,
"exp": 1601918501,
"jti": "749c77e5-bac0-43f1-aeea-1618ada0224f", (unique identifier for this token)
"mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3" (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
access_token body:
{
"iss": "ABC Service", (The Issuer of the token)
"sub": "4953fag3-ec5e-4ed3-b09e-d847f3f376c6", (The user_id, subject of the token)
"aud": "UI", (audience who will use the token)
"typ": "access", (type)
"iat": 1599326501,
"exp": 1599412901,
"jti": "3d2985ef-e767-495e-af88-448fc0ecb167", (unique identifier for this token)
"mid": "e392692b-6d77-49a9-9928-ac3c3d5208a3", (unique identifier for access-refresh token pair - refers to map_id in login_sessions table)
}
"fname": "john",
"lname": "doe",
"roles": [99b18377-5b4c-4e68-8ff4-bac4aea93bd2]
"other_key": "other_value"
Generating access token from refresh token:
Once access token is expired, we hit authorization server api to get the access token, while passing the refresh_token (jwt) in the body. The authorization server will have this API exposed which will accept refresh token and perform the following steps and then return access token.
Steps:
1. Verify the refresh_token with public key (whose counterpart private formed the token initially)
2. Pick the mid(map_id) from the token body
3. Get login session record containing this mid from the login_sessions table. Go to next step if there exists some login_session record.
4. If the status of the login_session record is ACTIVE, create one access_token (using the private key) with relevant details in it's body
5. Send back the access token as a response.
If any of the steps 1-4 fail above, we send back an error response with relevant message.
Upvotes: 2