JWT auth in Go
By: Tanveer Hassan
Tags: API, Jwt, Programming, Golang, Tutorial

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

Make the path appropriate for your workspace.

mkdir -p go/src/github.com/war1oc/jwt-auth

I use Dep for dependency management.

dep init

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"))

}

Run dep ensure to install the newly added dependency (“github.com/labstack/echo”).

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:

package main import (

"net/http"

"time" "github.com/dgrijalva/jwt-go"

"github.com/labstack/echo"

) type handler struct{}

//

func (h *handler) login(c echo.Context) error {

username := c.FormValue("username")

password := c.FormValue("password") // Most of the code is taken from the echo guide// https://echo.labstack.com/cookbook/jwt func (h *handler) login(c echo.Context) error {username := c.FormValue("username")password := c.FormValue("password") // Check in your db if the user exists or not

if username == "jon" && password == "password" {

// Create token

token := jwt.New(jwt.SigningMethodHS256) // Set claims

// This is the information which frontend can use

// The backend can also decode the token and get admin etc.

claims := token.Claims.(jwt.MapClaims)

claims["name"] = "Jon Doe"

claims["admin"] = true

claims["exp"] = time.Now().Add(time.Hour * 72).Unix() // Generate encoded token and send it as response.

// The signing string should be secret (a generated UUID works too)

t, err := token.SignedString([]byte("secret"))

if err != nil {

return err

}

return c.JSON(http.StatusOK, map[string]string{

"token": t,

})

} return echo.ErrUnauthorized

}

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:

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!")

}) h := &handler{}

e.POST("/login", h.login) e.Logger.Fatal(e.Start(":1323"))

}

Run the app go run *.go

curl -X POST localhost:1323/login -d "username=jon&password=password" {"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.

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.

Take a look at echo’s JWT middleware:

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:

package main import (

"github.com/labstack/echo/middleware"

) var IsLoggedIn = middleware.JWTWithConfig(middleware.JWTConfig{

SigningKey: []byte("secret"),

})

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.

Let’s create a restricted api:

In main.go:

e.GET("/private", h.private, isLoggedIn)

In handler.go:



//

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+"!")

} // 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+"!")

Run the app go run *.go

If we now hit the private api without any token we get an error:

curl localhost:1323/private {"message":"missing or malformed jwt"}

With token:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTQyNjkwMjQ3LCJuYW1lIjoiSm9uIERvZSJ9.OqsaJ76nYhiaiVPcAr13_vMPyTfRcv6eKFm06O3n8fE" localhost:1323/private 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.

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("/admin", h.private, isLoggedIn, isAdmin)

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 /admin api:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTU0MjgwMjIwMywibmFtZSI6IkpvbiBEb2UifQ.kuypz_fyVlnvMOweU6izkjuKTKOjPlJTYV-q3iT3pHg" localhost:1323/admin {"message":"Unauthorized"}

However, the private api still works fine:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6ZmFsc2UsImV4cCI6MTU0MjgwMjIwMywibmFtZSI6IkpvbiBEb2UifQ.kuypz_fyVlnvMOweU6izkjuKTKOjPlJTYV-q3iT3pHg" localhost:1323/private Welcome Jon Doe!

Changing the claim to insert admin as true, you will be able to access the /admin api:

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

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