A Working Solution to JWT Creation and Invalidation in Golang
Steven Victor
Posted on January 25, 2020
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:
- Set a reasonable expiration time on tokens
- Delete the stored token from client-side upon log out
- Have DB of no longer active tokens that still have some time to live
- Query provided token against The Blacklist on every authorized request.
Issues from the above-mentioned solutions:
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.
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:
- 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.
- 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.
- 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
manage-jwt
Then initialize go modules:
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:
.env
Creating JWT
From your project root directory(path: manage-jwt/), create the auth package(directory), then the auth.go file
auth
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.
model
a. Let's create the base_model.go file.
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
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
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:
service
Then, create the signin_service.go file:
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
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:
app
a. Routing
Then create the router.go file:
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
middlewares
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:
main.go
Then run:
go run main.go
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.
c. /todo endpoint: Let's create a todo with the token generated above.
This token will be added in the Authorization: Bearer Token
d. /logout endpoint
To logout, you must be authenticated, so add the above token in the Authorization: Bearer Token
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 ./...
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.
Posted on January 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.