Using JWTs in Golang

JSON Web Tokens are great for authentication and here is a simple way to implement them using Golang and Gin. The first step is to create a project and fetch the necessary requirements.

go mod init jwt-project-name
go get github.com/dgrijalva/jwt-go
go get github.com/gin-gonic-gin

It is important to remember that if there are any missing dependencies when adding code or functions that running

go get .

Can fetch missing dependencies that may have been forgotten about, or if the location is unknown to the user. It is good at fetching standard library dependencies and third party packages.

Next we want to create a function that can generate a token for us. This can be done many ways but one simple way for the sake of this post is to have a user send in a username and password pair of some sort and after authenticating that username and password pair, generate a token with specified claims.

We will have a Claims struct, and specify some endpoints that are authorized for a given user.

type Claims struct { Password string `json:"password"` Username string `json:"username"` Endpoints []string jwt.StandardClaims } var endpoints = []string{ "/users", }

Next, we will create function that generates our token.

func GenerateToken(password string, username string) (string, error) { claims := &Claims{ Password: password, Username: username, Endpoints: endpoints, StandardClaims: jwt.StandardClaims{ ExpiresAt: time.Now().Add(time.Hour * 24).Unix(), IssuedAt: time.Now().Unix(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte("secret-key")) }

So far, it is simple enough. We could generate a POST endpoint that accepts our username and password that would of course have to be validated some how. For this post I don't think it is relevant how that is done. But a simple database check should validate that while generating the token, but a different solution could be valid as well. For now, I am just going to create an endpoint that accepts a username and password, and calls GenerateToken without any sort of validation on the username and password.

func main() { router := gin.Default() router.POST("/authenticate", authenticate) router.Run("localhost:1234") } func authenticate(c *gin.Context) { var claims auth.Claims if err := c.BindJSON(&claims); err != nil { c.IndentedJSON(http.StatusBadRequest, gin.H{"Status": "Failed to generate token"}) } token, _ := auth.GenerateToken(claims.Password, claims.Username) c.IndentedJSON(http.StatusOK, gin.H{"jwt": token}) }

Now we need to add a "/users" endpoint, which is what we defined in our claims. This endpoint will need to be authorized for our authenticated user, but since we already added the endpoint to our claims, we just need to add middleware to handle that authorization. We simply defined a []string in our code to generate a token. When generating a token we set our "Endpoints" claim to a slice that we hardcoded, in reality these would come a database configuration or somewhere other than a slice in code.

When we define our "/users" endpoint, we also add middleware that can handle our authorization. Our middleware can go with our authentication logic, and it will verify that the token is valid, that the endpoint we are trying to hit is authorized, and either return a 401 response or call our function that we pass into our handler, which is our endpoint function. First, I will show the handler middleware that we need.

func AuthHandler(handlerFunc gin.HandlerFunc) gin.HandlerFunc { return func(c *gin.Context) { tokenString := c.GetHeader("Authorization") if tokenString == "" { c.AbortWithStatus(http.StatusUnauthorized) return } token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return []byte("secret-key"), nil }) if err != nil || !token.Valid { c.AbortWithStatus(http.StatusUnauthorized) return } claims, authorized := token.Claims.(jwt.MapClaims) if !authorized { c.AbortWithStatus(http.StatusUnauthorized) return } endpoint := c.Request.URL.Path for _, val := range claims["Endpoints"].([]interface{}) { if val == endpoint { handlerFunc(c) return } } c.AbortWithStatus(http.StatusUnauthorized) } }

Alright, this AuthHandler function is our middleware for performing authentication and authorizing our claims. This takes our endpoint that we specify below, and our gin.Context. It takes the gin.Context and fetches the Authorization header. It grabs the token from the header, and searches for the endpoint claim that matches our endpoint call. If it is there, it calls the endpoint function like we expect. Otherwise, it returns a 401 response. Below is how we use this middleware logic in our gin application.

func main() { router := gin.Default() router.POST("/authenticate", authenticate) router.GET("/users", auth.AuthHandler(users))//This is our new line for middleware router.Run("localhost:1234") } func users(c *gin.Context) { users := []string{ "Janzen", "John Doe", "Jane Doe", "Stewie", } c.IndentedJSON(http.StatusOK, gin.H{"users": users}) }

This code ensures that when we call the "/users" endpoint, the first thing we do is pass our function and context to our AuthHandler middleware. If everything is valid with our token and our claims, we can then proceed to call our users function which will return a list of users for us.

If we want to verify that this works, we can create a new endpoint that uses our middleware without actually having the claim added to our "Endpoints" claim. Since our middleware AuthHandler function will never find our endpoint claim, it will return a 401 status indicating that our request is unauthorized.