A guide to JWT authentication in Go

by:

Web Development

JSON Web Tokens (JWTs) are a popular method for dealing with online authentication, and you can implement JWT authentication in any server-side programming language.

For background reading JWTs in general, I recommend learning more about JWTs, best practices, and securing RESTful APIs with JWTs with these articles on the LogRocket blog.

This article is aimed at helping you get started with implementing JWT authentication in your Go web applications using the golang-jwt package.

The golang-jwt package is the most popular package for implementing JWTs in Go, owing to its features and ease of use. The golang-jwt package provides functionality for generating and validating JWTs.

Prerequisites

You’ll need to meet these basic requirements to get the most out of this tutorial.

  • Go 1.16 or later installed on your machine (for security reasons)
  • Experience building web applications in Go or any other language (optional)

Table of Contents

Getting started with the Golang-JWT package

After setting up your Go workspace and initializing the Go modules file go.mod, run this command on your terminal in the workspace directory to install the golang-jwt package:

go get github.com/golang-jwt/jwt

Once you’ve installed the golang-jwt, create a Go file and import these packages and modules.

import (
   "log"
    "encoding/json"
   "github.com/golang-jwt/jwt"
   "net/http"
   "time"
)

You’ll use these packages in this tutorial to log errors, set up a server, and set the token expiration time.

Setting up a web server in Go

Let’s start with creating a simple web server with an endpoint that will be secured with a JWT.

func main() 
   http.HandleFunc("/home", handlePage)
   err := http.ListenAndServe(":8080", nil)
   if err != nil 
      log.Println("There was an error listening on port :8080", err)
   


The main function sets up the home endpoint with a handler function handlePage that you’ll set up. The handlePage function will secure the page using JWTs. The server is set to listen on port :8080, but you can use any port of your choice.

The handlePage handler function will return the encoded JSON of the Message struct as a response to the client if the request is authorized after the request body is encoded.

type Message struct 
        Status string `json:"status"`
        Info   string `json:"info"`


func handlePage(writer http.ResponseWriter, request *http.Request) 
        writer.Header().Set("Content-Type", "application/json")
        var message Message
        err := json.NewDecoder(request.Body).Decode(&message)
        if err != nil 
                return
        
        err = json.NewEncoder(writer).Encode(message)
        if err != nil 
                return
        

The handlePage function, at this point, isn’t authenticated and making requests to the page will work freely. You’ll learn how to add authentication to your handler functions later in this tutorial.

API network page

Generating JWTs for authentication using the Golang-JWT package

You will need a secret key to generate JWT tokens using the golang-jwt package. Here’s an example private key for this tutorial; however, you should use a cryptographically secure string for your secret key and load it from an environment variables file (.env).

Check out this article to learn how to use environment variables in your Go applications.

var sampleSecretKey = []byte("SecretYouShouldHide")

Kindly note that whoever has the secret key you use for your JWTs can authenticate users of your application. The sampleSecretKey variable holds the private key in this case.

Here’s a function for generating JWT tokens. The function should return a string and an error. If there’s an error generating the JWT, the function returns an empty string and the error. If there are no errors, the function returns the JWT string and the nil type.


More great articles from LogRocket:


func generateJWT() (string, error) 


You can create a new token using the New method of the JWT package. The New method takes in a signing method (the cryptographic algorithm for the JWT) and returns a JWT token.

token := jwt.New(jwt.SigningMethodEdDSA)

If you want to modify the JWT, you can use the Claims method of the token.

claims := token.Claims.(jwt.MapClaims)
claims["exp"] = time.Now().Add(10 * time.Minute)
claims["authorized"] = true
claims["user"] = "username"

In this case, you’re setting an expiry time for the JWT, which is ten minutes, using the time module and the username and authorization status. You’ll be able to retrieve the claims when attempting to verify the JWT.

The final part of generating a JWT is to sign the string using your secret key. You can sign your token string using the SignedString method of the token. The SignedString method takes the secret key and returns a signed token string.

tokenString, err := token.SignedString(sampleSecretKey)
if err != nil 
    return "", err
 

 return tokenString, nil

In cases where there are errors signing the token, you can return an empty string and the error.
Unlike cookies, you don’t need to store JWT; all you need is your signing key to verify tokens.

Verifying JWT tokens

The conventional method of verifying JWTs uses middleware (handler functions that take in other handler functions for operations). Here’s how to use middleware to verify that a request is authorized.

func verifyJWT(endpointHandler func(writer http.ResponseWriter, request *http.Request)) http.HandlerFunc 


The verifyJWT function is a middleware that takes in the handler function for the request you want to verify. The handler function uses the token parameter from the request header to verify the request and respond based on the status.

 return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) 

)

The verifyJWT function returns the handler function passed in as a parameter if the request is authorized.

The first step to verifying JWTs is to inspect the token in the request’s header.

if request.Header["Token"] != nil 


If there’s a token, you can proceed to verify the token and verify claims.

You’ll have to parse the token, and you can parse the token using the Parse method of the jwt package. The parse method takes in the token and a JWT decorator function and returns an interface and an error.

You need to use the same signing method you used to sign the token when you generated it to verify the signature using the Method method of the token. In this case, the signing method was the ECDSA method.

token, err := jwt.Parse(request.Header\["Token"\][0], func(token *jwt.Token) (interface, error) 
            _, ok := token.Method.(*jwt.SigningMethodECDSA)
            if !ok 
               writer.WriteHeader(http.StatusUnauthorized)
               _, err := writer.Write([]byte("You're Unauthorized!"))
               if err != nil 
                  return nil, err

               
            
            return "", nil

         )

If the signature verification fails (the function returns !ok), you can return a StatusUnauthorized header to the client.

if err != nil 
               writer.WriteHeader(http.StatusUnauthorized)
               _, err2 := writer.Write([]byte("You're Unauthorized due to error parsing the JWT"))
              if err2 != nil 
                      return
                

In the code above, there’s an error parsing the token. Therefore, the user is unauthorized, and you can write a message and return an unauthorized status.

You can validate the token using the Valid method of the token.

if token.Valid 
                      endpointHandler(writer, request)
                         else 
                                writer.WriteHeader(http.StatusUnauthorized)
                                _, err := writer.Write([]byte("You're Unauthorized due to invalid token"))
                                if err != nil 
                                        return
                                

If the token is valid, you can pass in the endpoint handler with the writer and request parameters of the handler function for the middleware function to return the endpoint.

Here’s the else statement for a case where there’s no token in the header of the client’s request:

else 
          writer.WriteHeader(http.StatusUnauthorized)
          _, err := writer.Write([]byte("You're Unauthorized due to No token in the header"))
           if err != nil 
               return
           

Since you’re using middleware, the handler function in your route declaration will be the verifyJWT middleware with the handler function for the route as the argument.

http.HandleFunc("/home", verifyJWT(handlePage))

Once you’ve added your verification function to the route, the endpoint is authenticated.

Endpoint authenticated

On the client side, the client has to provide an issued token. Here’s a function that uses the generateJWT function to add tokens in requests.

func authPage(writer http.ResponseWriter, ) 
        token, err := generateJWT()
        if err != nil 
                        return
         
        client := &http.Client
        request, _ := http.NewRequest("POST", "<http://localhost:8080/>", nil)
        request.Header.Set("Token", token)
        _, _ = client.Do(request)


In the authPage function, the token variable holds the token from the generateJWT function. Using a reference to the Client type of the http package, you can create a new client and make a request to the endpoint. The request variable is the request instance and — using the Set method of the header method of the request instance — you can set the token in the request header as shown above.

You can also choose to set the token as a cookie and retrieve it for verification whenever a client makes a request to the authenticated endpoint.

When you’re generating a JWT, you can choose to embed information in the token. In the generateJWT function, you added the username variable to the claims map.

Here’s how you can extract the claims, using the username claims as an example. You can use middleware or add the functionality to your verification function when verifying the token signature.

func extractClaims(_ http.ResponseWriter, request *http.Request) (string, error) 
        if request.Header["Token"] != nil 
                tokenString := request.Header\["Token"\][0]
                token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface, error) 

          if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok 
                return nil, fmt.Errorf("there's an error with the signing method")
          
                return sampleSecretKey, nil

            )

            if err != nil 
                        return "Error Parsing Token: ", err
                

In the extractClaims functions, the process is the same as the verifyJWT function; you retrieved the token from the header, parsed the token, and verified the signature.

claims, ok := token.Claims.(jwt.MapClaims)
          if ok && token.Valid 
                username := claims["username"].(string)
                return username, nil
          

        
        return "unable to extract claims", nil

On validating the token, you can retrieve the claims using the Claims method and use the claims map to retrieve the data in the JWT, as shown above.

Conclusion

This tutorial taught you how to use JWT authentication to authenticate your API and web page endpoints in Go with JSON Web Tokens by using the golang-jwt package. You can find the complete code in this tutorial as a Github Gist.

Remember to use environment variables for your secret keys and do not hide sensitive data in JWTs. There are many JWT tutorials on the LogRocket blog that you can check out to get started in the language or framework you’re interested in using!

LogRocket: Full visibility into your web and mobile apps

LogRocket Dashboard Free Trial Banner

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page web and mobile apps.

Try it for free.

Leave a Reply

Your email address will not be published.