JWT auth in Go Part 2 — Refresh Tokens

Tanveer Hassan
Monstar Lab Bangladesh Engineering
5 min readMay 21, 2019
Photo by Matt Artz on Unsplash

Authentication tokens generally come in pairs. Access and Refresh tokens. I have talked about basic JWT authentication using access tokens in a previous article. This is a continuation of that.

Access tokens are used to identify a user without tapping into the database. Say we need the logged-in user’s id to create a blog post they are submitting. We make sure it has not been tampered with and retrieve the user id from the token. However, the access token should have an expiration. Once in a while, we need to check with the system and the database if the user can log in or perform certain operations.

This is where refresh tokens come in. Refresh tokens are used for, you guessed it, refreshing expired access tokens. After an access token expires, the refresh token is used to get a new pair of access and refresh tokens. This time, it is done by verifying the user against the database. The user might be deleted or banned and thus cannot access the service anymore. Access tokens are generally short lived and expire a few minutes after they are issued. Refresh tokens on the other hand usually have a longer duration.

The access token can also get compromised, and the user can revoke the refresh tokens. The attacker will have much less time before the access token expires and the server revokes the session.

Refresh tokens don’t really need all the claims which we put into access tokens. They are used to only identify a user and fetch their data. Thus we can get by with only the user’s ID (primary key).

You may want to read Part 1 before proceeding.
I will also be coding over the project from Part 1.

In part 1, our login endpoint was returning a single token as a success response:

curl -X POST localhost:1323/login -d "username=jon&password=password"{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTQyNjkwMjQ3LCJuYW1lIjoiSm9uIERvZSJ9.OqsaJ76nYhiaiVPcAr13_vMPyTfRcv6eKFm06O3n8fE"}

This was our access token. And it expires after 72 hours of being issued. 72 hours is way too long of an expiration time for an access token. We’ll change it to be 15 minutes.

handler.go:

claims["exp"] = time.Now().Add(time.Minute * 15).Unix()

You can verify the change by logging in and using the online debugger. You’ll see the token expires in the next 15 minutes.

Let’s generate, sign, and send a refresh token with the login response as well:

refreshToken := jwt.New(jwt.SigningMethodHS256)
rtClaims := refreshToken.Claims.(jwt.MapClaims)
rtClaims["sub"] = 1
rtClaims["exp"] = time.Now().Add(time.Hour * 24).Unix()
rt, err := refreshToken.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, map[string]string{
"access_token": t,
"refresh_token": rt,
})

Now when you log in again, you’ll receive a refresh token along with the access token.

curl -X POST localhost:1323/login -d "username=jon&password=password"{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTU4MzI3NTAyLCJuYW1lIjoiSm9uIERvZSIsInN1YiI6MX0.ih9ir293p8QIoh8wJqiHt0yKwQ-4FvYgrVqeICkIF40",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NTg0MTMwMDIsInN1YiI6MX0.jnszRI4V1Z01JTzfKlR_WsMOMlUvPqOkfp8_sEeTaO0"
}

You can debug the contents of each token at https://jwt.io/

Payload for the access_token:

{
"admin": true,
"exp": 1558327502,
"name": "Jon Doe",
"sub": 1
}

Payload for the refresh_token:

{
"exp": 1558413002,
"sub": 1
}

As you can see, the contents for the token payloads are different. This is because we don’t need the extra information of the access token in the refresh token; we will pull the user from the database anyway.

sub contains the unique user id with which we identify the user.

For refreshing tokens we will have to generate another token pair, so let’s put that logic into another function. I’ll create another go file to contain the token generation logic.

token.go

Let’s write the refresh token api now. We will decode the token and figure out who the user is and if they are allowed to get a new pair of tokens.

We will pass the refresh token to the api in a POST form.

We need to validate the token and extract the claims from it, to do this, we need to parse the token.
I will be using the jwt-go library sample code.

Our new token refreshing handler:

handler.go

Here is a rundown of what I am doing in the above handler:

The Parse function takes the token string and a key function as parameters.
I take the token string from the form post body.
The Key Function has to return the key with which to validate the token, I verify that the signing method used to sign the token is HMAC and return the key, in this case it is a []byte containing the string “secret”.

Once the token has been validated, I extract the claims, take the sub claim, run it through my own business logic, in this case it has to be 1. If my logic is satisfied that the user is allowed to refresh their token, I generate a new token pair and send it back.

Conclusion

Using refresh tokens in tandem with access tokens can bolster the security for you application. It is a balance between security and performance. If you use non-expiring access tokens, the user never goes through database calls etc when accessing a private resources, however it also poses a threat when the token gets compromised. Using short lived access tokens to access resources and refreshing them using a long lived refresh tokens balances this out and improves security for the user.

This was a very simple and crude implementation of jwt authentication and refreshing expired tokens. If you have not already noticed, this code is not production ready. I have tried putting it together in such a way so that all the focus remains on the jwt part and not other details such as database, business logic, architecture, etc. One might say I mocked all of those out to build an isolated unit to only test refresh token logic.

Get the source code from Github:

I encourage you to go through the Part 1 of this series if you haven’t already:

--

--