JWT authentication in Golang with Echo

Authentication is the most fundamental building block of any application. In this article we will implement JWT authentication



Authentication is the most fundamental building block of any application. Whenever we start building a new app, we consider how users authenticate in the very beginning. In the olden days, we used to implement session based authentication and transmitted the data using cookies.

We had to store the session data somewhere. That used to make our apps stateful. But this came with a problem. We had to make database calls whenever we wanted to authenticate someone. This causes huge overhead and does not scale well. It can also create a bottleneck in our application.

Using JSON Web Tokens can mitigate this issue. We don’t have to store any session data in our database or anywhere because JWTs can carry information with them in JSON format. Although they can be encrypted, we will be focusing on signed tokens which carry the authenticated user’s information.

As you will see later in this article, we don’t have to query the database for the requesting user’s information for making restricted api calls. Signed tokens cannot be tampered or else the signature will not match.
I highly recommend going through the following writing to learn more about the structure of JWTs here

. . .

To get started, we need to create a new Go application:

Make the path appropriate for your workspace & install deps.
mkdir go-jwt
go get github.com/labstack/echo
go get github.com/dgrijalva/jwt-go

I will be using the Echo Framework. It’s a very minimalist framework which has the essentials baked in. Let’s create the main.go file by taking the code from the Echo Guide. This will be our starting point.

main.go:
package main import ( "net/http" "github.com/labstack/echo" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }

If you run the application now, echo fires up a server and listens on the :1323 port. A basic hello world application.
. . .

Let’s create the login handler

handler.go
Since database connection and querying is not in the scope of this article, I checked the username and password this way.

This is a very minimal application describing the core of JWT in Go.
We need to add a route for login:

main.go
Now run the app to test our login handler is working:
go run *.go

Test /login route
curl -X POST localhost:1323/login -d "username=jon&password=password"

Output:
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTQyNjkwMjQ3LCJuYW1lIjoiSm9uIERvZSJ9.OqsaJ76nYhiaiVPcAr13_vMPyTfRcv6eKFm06O3n8fE"}

You should get the token in the response when you hit the api with the correct username and password.

Incorrect username and password will throw an unauthorised error.
curl -X POST localhost:1323/login -d "username=jon&password=nope"
{"message":"Unauthorized"}

Since the token is being generated using exp (expiration), it will be unique everytime.

Let’s inspect how our token looks when decoded, head over to jwt.io and paste the token:

Header:
{ "alg": "HS256", "typ": "JWT" }

Payload:
{ "admin": true, "exp": 1542690247, "name": "Jon Doe" }

Both front and backend can use the payload to identify the user.
. . .

Let’s create a restricted api

Now we can restrict apis and require users to be logged in. This can be accomplished by middlewares. We can also restrict by claims such as if a user is an admin or not.

We create a middleware of our own which is basically just the echo jwt middleware, but we can now use it with our handlers.

middleware.go:
The signing key has to match the signing key used when issuing the token. It’s better to keep it in a configuration file or environment variable.

In handler.go:
// Most of the code is taken from the echo guide // https://echo.labstack.com/cookbook/jwt func (h *handler) private(c echo.Context) error { user := c.Get("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) name := claims["name"].(string) return c.String(http.StatusOK, "Welcome "+name+"!") }

In main.go add the private route:
e.GET("/is-loggedin", h.private, IsLoggedIn)

Lets run our app:
app go run *.go

If we now hit the private api without any token we get an error:
curl localhost:1323/is-loggedin
{"message":"missing or malformed jwt"}

With token (Dont forget to replace JWT token with one you created recently):
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTQyNjkwMjQ3LCJuYW1lIjoiSm9uIERvZSJ9.OqsaJ76nYhiaiVPcAr13_vMPyTfRcv6eKFm06O3n8fE" localhost:1323/is-loggedin
Welcome Jon Doe!

And there you have it! Authentication with JWT. As you may have already noticed, we did not make a database call but got the user’s name from the token itself.

This has tremendous performance advantages over making db query for every api call the client makes. We can extract necessary information such as user ID and role from the token to decide whether we want to allow the user access to the api or not.
. . .

Let’s create a Admin api

Now that we have a basic jwt auth system running, let’s go a step further and create a middleware for admin (isAdmin).

middleware.go:
func isAdmin(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { user := c.Get("user").(*jwt.Token) claims := user.Claims.(jwt.MapClaims) isAdmin := claims["admin"].(bool)
if isAdmin == false { return echo.ErrUnauthorized }
return next(c) } }

main.go:
e.GET("/is-admin", h.private, IsLoggedIn, isAdmin)

Lets run our app:
app go run *.go

Test admin route:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTQyODAyMzk5LCJuYW1lIjoiSm9uIERvZSJ9.K-uDZN_XX0J9PaUJXeRGjHmtfDwLr9StGZG1FOIa5Hc" localhost:1323/is-admin Welcome Jon Doe!

Middlewares are also like handler functions, and you can add as many as you want. In this case, the isLoggedIn middleware sets the user to the context and thus we can use it in our isAdmin middleware which only checks whether the user is an admin or not.

Change the claim admin to false in the login handler:
claims["admin"] = false

Log in again and using the new token try hitting the /is-admin api:
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTU0MjgwMjIwMywibmFtZSI6IkpvbiBEb2UifQ.kuypz_fyVlnvMOweU6izkjuKTKOjPlJTYV-q3iT3pHg" localhost:1323/is-admin
{"message":"Unauthorized"}

You can create middlewares which satisfy your business use cases and give access to users accordingly.
. . .

Refresh Token

Let’s generate, sign, and send a refresh token with the login response as well:
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"
{"refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NjEyMTQ2OTYsInN1YiI6MX0.dCS3PU0dY-ZLYZBPzlg0_XoGC9lgwn2ESUf8I_igkzE","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTYxMzg3NDk2LCJuYW1lIjoiSm9uIERvZSJ9.wNrJoCO3yc7fHQ62yELx2tw3HCD1wZoD9w0BslvfhOo"}

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.
. . .

Refresh Token api

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:

token.go
The Parse function takes the token string and a key function as parameters.  I take the token string from the form post body. Lets try it out
curl --header "Content-Type: application/json" \
> --request POST \
> --data '{"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzEzMzA1NjgsInN1YiI6MX0.AdOYNdum2rjlzR7VwUnUyX5YdBy3mi8Uzd8ksEvGxWk"}' \
> localhost:1323/refresh
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTcxMjQ1MDk0LCJuYW1lIjoiSm9uIERvZSIsInN1YiI6MX0.KQtnu_fDih63gnVrwh-qQbBrSSpiXJ62Kru5LBWehgo","refresh_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzEzMzA1OTQsInN1YiI6MX0.nagGlEUZlKSwRNGoCVn8sLXBIjN0ltf4WqaPV9IL87g"}
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.

On a mission to build Next-Gen Community Platform for Developers