DEV Community

Cover image for A Working Solution to JWT Creation and Invalidation in Golang
Steven Victor
Steven Victor

Posted on • Updated on

A Working Solution to JWT Creation and Invalidation in Golang

Before you proceed

This is part 1 of the two-part series.
This part solely focused on creating a JWT that has no expiration date. While this seems cool, it can have security issues. For example, in the event that the JWT is hijacked. If this happens the only remedy in this article is, when the authenticated user logs out, the token will be revoked and can't be used any further(by either the hijacker or the user). So the user can obtain a new one when he login again.

In part 2 of this series, Two tokens will be used:

  • Short Term Token: also known as access token(usually 15 mins)
  • Long Term Token: also known as refresh token(usually 1 week) This approach is more secure compared to the above. Once the article is ready, the link will be provided here.

Introduction

Whenever JWT is mentioned to be used for authentication, there is this question that is always asked by developers that have experience using it:
when a user logout, what happens to the JWT?
That question is a nightmare.

What this article solved: Once that user logs out, the JWT used is invalidated that very second! Without waiting for any expiration time if any was set when the token was created.

Using my Github account as a case study. I don't get logged out except I explicitly trigger the logout button.
When using JWT, many have advocated that the token should have a short time span(15min, 20min, etc), the token should be revoked often time than not, blah, blah, blah.
Think about it, if a token lifespan is short, say 10mins, it means, after that period, you will be logged out and need to log in again😡. Except for applications that require that(payment applications, etc), I see no reason why I should go through this pain.

Solutions to the JWT discussion before now from this reading, include:

  1. Set a reasonable expiration time on tokens
  2. Delete the stored token from client-side upon log out
  3. Have DB of no longer active tokens that still have some time to live
  4. Query provided token against The Blacklist on every authorized request.

Issues from the above-mentioned solutions:

  1. What if you set the expiration of 1 hour for a JWT, then the user login, then logout after like 1 minute or so? What it means is, that JWT will be valid for an additional 59 minutes; ample time for a hacker to do his thing💀, in the event that the user JWT was hijacked.

  2. About Blacklist stuff. This is what that means: Once a user logs out, add the token he used to a blacklist table in your database(Redis preferably). So once the user wants to perform a request that requires authentication and provides that token, then blacklist table will be queried to check if a token has been created before by that user and have not expired, if found, don't allow the user to get away with his mischievous act, hold him right there✊.
    While this approach seems cool, it has obvious downsides:
    When creating the token(during signup or login), I must specify a time when the token expires in my code. When that time elapses, the user will be forced to login again. I definitely don't want this.
    You might argue that you can create a token that does not expire right? Well, what it means is that your blacklist table will soon have "zillions" of rows of JWT tokens not used by anyone, and you cannot afford to delete any because a user might wait patiently and reuse a token he has been saving for like a year now, just to test the integrity of your application🧐.

This is what this article is all about:

  1. We want a type of application in which the user does not try to access his account after a few minutes or hours and discover that he is logged out because we are trying to protect them from hackers💀. Don't get me wrong, it all depends on the time of application. For instance, my banking application logs me out after say 5min of inactivity, which is a good use case.
  2. We want an application in which, when the user chooses to logout, he does so, and can't try to use that same JWT he used before logging out for any authenticated request. He should not be allowed.
  3. We don't an application that has "zillions" of rows of blacklisted JWTs. Total no! I mean, why waste resources?

So this is what we want:
An application that can keep the user logged in forever except the user chooses to explicitly logout. I don't know about you, but this is how I
use my Twitter, Github, and so on.

To achieve this in your application, this piece is for you. Especially when you want to use JWT.

This is the trick I used:
I created a database table called auths, the table has three columns: id, user_id and auth_uuid. Pay attention to the auth_uuid. It is created from uuid. UUID stands for a universally unique identifier. When a user login, a JWT is created. The user_id and auth_uuid are used as claims for that JWT. The **user_id is the id of the user who attempts to login, while the auth_uuid is created using a helper package called: twinj. When a user logs out, the created row(of the user_id and the auth_uuid is deleted from the auths table). What it now means is, though the JWT has not expired, it cannot be used to make any further requests on behalf of that user.
Reason: because part of its claims are deleted. For that user to make any authenticated request again, he needs to login, which will create a new JWT for him then also a new row is added to the auths table, with the user_id and a brand new auth_uuid. Take note that a new uuid is created for each JWT according to the code implementation you will below.
If what was explained above is not clear, please look at the example below where it was demonstrated.

Building

Consider a simple Todo Restful API with Authentication.

Basic StepUp

a. From any location, you prefer in your computer, create a directory called manage-jwt

mkdir manage-jwt

b. Change to that directory

cd manage-jwt

Then initialize go modules:

go mod init manage-jwt

c. Environmental variables.
We will store all our environmental variables in .env file.
From the root directory, create the .env file:

touch .env

Creating JWT

From your project root directory(path: manage-jwt/), create the auth package(directory), then the auth.go file

mkdir auth

cd auth && touch auth.go

From the file above, we created the JWT with the UserId and a AuthUuid, as seen in the AuthDetails struct. We also have functions that verify the token and extract the UserId and the AuthUuid.

Wiring the Models

From the root directory, create the model directory. This is where we will have our database initialization and all database related stuff.

mkdir model

a. Let's create the base_model.go file.

cd model && touch base_model.go


From the above file, we have the Initialize method and an interface that is a collection of our model methods we will define soon.

b. Create the user model

touch user.go

We have methods that validate the email, create a user and get a user by email.
Since the sole aim of this article is about jwt, we left implementation as basic as possible.

c. Create the todo model

touch todo.go


As seen in the above file, we just have the CreateTodo model, since we are focused on JWT.

d. Create the auth_uuid model

touch auth_uuid.go

We functionalities to create, get and delete the uuid and the user id associated with the jwt. For instance, the CreateAuth method is used in the Login controller function(this will be defined later), the auth created have the AuthUuid and the UserId. These are then used as claims when creating the JWT.
Whenever a request is made that requires authentication, the FetchAuth method is called, which lookup the auths table and check for the auth_uuid and the user_id. If they exist, the next line of action is taken(such as Creating a Todo, Logging out the user, etc).
The DeleteAuth method is used to delete the auth_uuid and user id from the auths table. This happens during logout, thus, rendering that JWT useless😪 because it cannot be used for any other request. Ever!.

Take note that the JWT is not deleted. It still exists, but it is invalid because the claims used to form it are no more. What actually made this possible is the uuid. Since the uuid is unique at its creation, there is little or no chance of having the same uuid in auths table. Even if that eventually happens, the user id is ever unique. We can't two or more same user id in the auths table.
This is how the uuid table looks like:

id user_id auth_uuid
1 1 83b09612-9dfc-4c1d-8f7d-a589acec7081
----- ---------- ---------------------------------------
2 2 14033612-df45-sdf3-137d-dfsdfd32243d

So, when the DeleteAuth method is called, the row that matches the parameters provided is deleted. Then no further request can be carried out with that JWT again because the auths table will always be checked👨‍✈️.

Wire up the Signin Service

Before any authenticated request is made, the user needs to the signed in. This is where the CreateToken function from the auth package is called.

From the root directory, create the service directory:

mkdir service

Then, create the signin_service.go file:

cd service && touch signin_service.go

This would simply have been done without an interface. The purpose of defining the method in an interface is to enable us to mock it when writing test cases.
Observe that we passed as parameter the AuthDetails struct which defines the auth_uuid and the user id; which are used as claims when creating JWT.

Wire up the Controllers

From the project root(path: /manage-jwt), create the controller directory(package).

a. The User Controller
Create the user_controller.go file

cd controller && touch user_controller.go

Keeping things super simple, so as not to distract the main purpose of the article.

b. The Login Controller
A user can login after he has signed up(been created).
Create the login_controller.go file

touch login_controller.go

As seen in the file above, we have both the Login and the LogOut functions.
Observe in the Login function that we created a new row in the auths table when we called CreateAuth method, we then passed its return value to the SignIn method, which calls the CreateToken function.
So now, we have a JWT and a row in the auths table that has the auths and the user id used as claims when creating the token.

From the LogOut function, called the DeleteAuth method that deleted that row created in the auths table, thus rendering the JWT useless.

Note: Remember that before you logout, you must be authenticated, so you must add to the header of your request a valid JWT.

c. The Todo Controller
Create the todo_controller.go file.

touch todo_controller.go

Creating a todo requires a user to be authenticated. From the CreateTodo function, we extracted the JWT claims(particularly AuthUuid and the UserId). We then checked the validity of those by calling FetchAuth method. If everything goes well, we then proceed to create the todo.

Routing and Starting the Application

Let's connect everything together and fire up the application.
From the root directory, create the app directory:

mkdir app

a. Routing
Then create the router.go file:

cd app && touch router.go

Observe that we called a middleware we have not created yet and we used a router. We will create these shortly.

b. The StartApp function
Still, in the app directory, create the app.go file:

touch app.go

The above file defined the router variable referenced in the router.go file. It also called the Initialize method(defined in the model), for the database connection. We called the route() function and also started the application.

c. The Middleware
From the root directory(path: manage-jwt/), create the middlewares directory, then the middlewares.go file

mkdir middlewares

cd middlewares && middlewares.go

The TokenAuthMiddleware function help to protect routes that require authentication.

d. The main.go file
It is time to finally test our hard work💪. From the root directory, create the main.go file and call the StartApp function defined above:

touch main.go

Then run:

go run main.go
Enter fullscreen mode Exit fullscreen mode

Your app should be on fire🔥 if you followed the instructions above.

Testing with Postman

Let's try our hands on some endpoints

a. /user endpoint: for creating a user(signup). Provide an email address to be signed up.

Alt Text

b. /login endpoint
Alt Text

c. /todo endpoint: Let's create a todo with the token generated above.
This token will be added in the Authorization: Bearer Token

Alt Text

d. /logout endpoint
To logout, you must be authenticated, so add the above token in the Authorization: Bearer Token

Alt Text

Bonus

a. The API is deployed is to heroku. You can test using:
https://manage-jwt.herokuapp.com.

b. Test Cases are added for the above functionalities, get the Github repository here and run tests from the root directory using:

go test ./...
Enter fullscreen mode Exit fullscreen mode

c. Circle CI is used for Continuous Integration.

d. The application is already dockerized. You can run it on docker if you wish. Setup is found in the repo.

Conclusion

So, there you have it. The JWT monster has been trampled on our feet, as we can invalidate it at any time if we wish.

Get the repository here, which you can star to track any new update.

Test the API in production using the url: https://manage-jwt.herokuapp.com.

Happy Forcing JWT to be useless at will🤣.

Follow me on Twitter to get notified of current releases.

Also, follow here on dev.to

Thank you.

Top comments (5)

Collapse
 
branislavlazic profile image
Branislav Lazic

Although an interesting solution, there are few drawbacks to this approach, and generally, I feel that people are misunderstanding JWT. Take a look at your solution and ask yourself: "What advantages does it provide in comparison by just using auth_id as a session id?". Upon each request, you would check its existence and expiry time. Index auth_id and it will be super fast. Want to logout? Simply delete it from the database. In your case, you're calling a database every time you want to validate your JWT.

Now, what people fundamentally do not understand is that JWT is insecure by default and that JWT itself or its claims should not be stored anywhere. JWT should be VERIFIED and that's it. That's why it's fully stateless. If you want to have long lived sessions, simply introduce the refresh token which is persistent by nature. If a client tries to verify JWT against your API and gets an unauthorized response, then a client can retry a call by providing a refresh token. Fetch a refresh token from the database, check its validity, and issue a new JWT.

P.S. Always hash the refresh tokens. Storing refresh tokens in their plain form is equivalent to storing plain text passwords.

Collapse
 
riazosama profile image
Osama Riaz

What's the update on part 2?

Collapse
 
stevensunflash profile image
Steven Victor

Hi Osama. Check it here:
nexmo.com/blog/2020/03/13/using-jw...

Collapse
 
riazosama profile image
Osama Riaz

Thanks you

Thread Thread
 
stevensunflash profile image
Steven Victor

You're welcome