{ Soham Kamani }

AboutBlog GithubTwitter

Implementing JWT based authentication in Golang 🔐

(Updated on )

Authentication allows your application to know that the person who sending a request to your application is actually who they say they are. The JSON web token (JWT) is one method for allowing authentication, without actually storing any information about the user on the system itself (as opposed to session based authentication).

In this post, we will demonstrate how JWT based authentication works, and how to build a sample application in Go to implement it.

If you already know how JWT works, and just want to see the implementation, you can skip ahead, or see the source code on Github

The JWT format

Let’s say we have a user called user1, and they try to log into an application or website. Once successful they would receive a token that looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ.2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54

This is a JWT, and consists of three parts (separated by .):

  1. The first part is the header (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9). The header specifies information like the algorithm used to generate the signature (the third part). This part is pretty standard and is the same for any JWT using the same algorithm.
  2. The second part is the payload (eyJ1c2VybmFtZSI6InVzZXIxIiwiZXhwIjoxNTQ3OTc0MDgyfQ), which contains application specific information (in our case, this is the username), along with information about the expiry and validity of the token.
  3. The third part is the signature (2Ye5_w1z3zpD4dSGdRp3s98ZipCNQqmsHRB9vioOx54). It is generated by combining and hashing the first two parts along with a secret key.

Now the interesting thing is that the header and payload are not encrypted. They are just base64 encoded. This means that anyone can view their contents by decoding them.

For example, we can use this online tool and decode the header or payload.

Which will show its contents as:

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

 

If you are using linux or Mac OS, you can also execute the following statement on the terminal:

echo eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | base64 -D

Similarly, the contents of the payload are:

{"username":"user1","exp":1547974082}

How the JWT signature works

So if the header and signature of a JWT can be read and written to by anyone, what actually makes a JWT secure? The answer lies in how the last part (the signature) is generated.

Let’s pretend you’re and application that wants to issue a JWT to a user (for example, user1) that has successfully signed in.

Making the header and payload are pretty straightforward: The header is more or less fixed, and the payload JSON object is formed by setting the user ID and the expiry time in unix milliseconds.

The application issuing the token will also have a key, which is a secret value, and known only to the application itself. The base64 representations of the header and payload are then combined with the secret key and then passed through a hashing algorithm (in this case its HS256, as mentioned in the header)

jwt algorithm

The details of how the algorithm is implemented is out of scope for this post, but the important thing to note is that it is one way, which means that we cannot reverse the algorithm and obtain the components that went into making the signature… so our secret key remains secret.

Verifying a JWT

In order to verify an incoming JWT, a signature is once again generated using the header and payload from the incoming JWT, and the secret key. If the signature matches the one on the JWT, then the JWT is considered valid.

Now let’s pretend that you’re a hacker trying to issue a fake token. You can easily generate the header and payload, but without knowing the key, there is no way to generate a valid signature. If you try to tamper with the existing payload of a valid JWT, the signatures will no longer match.

jwt verification

In this way, the JWT acts as a way to authorize users in a secure manner, without actually storing any information (besides the key) on the application server.

Implementation in Go

Now that we’ve seen how JWT based authentication works, let’s implement it using Go.

Creating the HTTP server

Let’s start by initializing the HTTP server with the required routes:

package main

import (
	"log"
	"net/http"
)

func main() {
	// "Signin" and "Welcome" are the handlers that we will implement
	http.HandleFunc("/signin", Signin)
	http.HandleFunc("/welcome", Welcome)
	http.HandleFunc("/refresh", Refresh)

	// start the server on port 8000
	log.Fatal(http.ListenAndServe(":8000", nil))
}

We can now define the Signin and Welcome routes.

Handling user sign in

The /signin route will take the users credentials and log them in. For simplification, we’re storing the users information as an in-memory map in our code:

var users = map[string]string{
	"user1": "password1",
	"user2": "password2",
}

So for now, there are only two valid users in our application: user1, and user2. Next, we can write the Signin HTTP handler. For this example we are using the dgrijalva/jwt-go library to help us create and verify JWT tokens.

import (
  //...
  // import the jwt-go library
	"github.com/dgrijalva/jwt-go"
	//...
)

// Create the JWT key used to create the signature
var jwtKey = []byte("my_secret_key")

var users = map[string]string{
	"user1": "password1",
	"user2": "password2",
}

// Create a struct to read the username and password from the request body
type Credentials struct {
	Password string `json:"password"`
	Username string `json:"username"`
}

// Create a struct that will be encoded to a JWT.
// We add jwt.StandardClaims as an embedded type, to provide fields like expiry time
type Claims struct {
	Username string `json:"username"`
	jwt.StandardClaims
}

// Create the Signin handler
func Signin(w http.ResponseWriter, r *http.Request) {
	var creds Credentials
	// Get the JSON body and decode into credentials
	err := json.NewDecoder(r.Body).Decode(&creds)
	if err != nil {
		// If the structure of the body is wrong, return an HTTP error
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Get the expected password from our in memory map
	expectedPassword, ok := users[creds.Username]

	// If a password exists for the given user
	// AND, if it is the same as the password we received, the we can move ahead
	// if NOT, then we return an "Unauthorized" status
	if !ok || expectedPassword != creds.Password {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// Declare the expiration time of the token
	// here, we have kept it as 5 minutes
	expirationTime := time.Now().Add(5 * time.Minute)
	// Create the JWT claims, which includes the username and expiry time
	claims := &Claims{
		Username: creds.Username,
		StandardClaims: jwt.StandardClaims{
			// In JWT, the expiry time is expressed as unix milliseconds
			ExpiresAt: expirationTime.Unix(),
		},
	}

	// Declare the token with the algorithm used for signing, and the claims
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	// Create the JWT string
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		// If there is an error in creating the JWT return an internal server error
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	// Finally, we set the client cookie for "token" as the JWT we just generated
	// we also set an expiry time which is the same as the token itself
	http.SetCookie(w, &http.Cookie{
		Name:    "token",
		Value:   tokenString,
		Expires: expirationTime,
	})
}

If a user logs in with the correct credentials, this handler will then set a cookie on the client side with the JWT value. Once a cookie is set on a client, it is sent along with every request henceforth. Now we can write our welcome handler to handle user specific information.

Handling post authentication routes

Now that all logged in clients have session information stored on their end as cookies, we can use it to:

  • Authenticate subsequent user requests
  • Get information about the user making the request

Let’s write our Welcome handler to do just that:

func Welcome(w http.ResponseWriter, r *http.Request) {
	// We can obtain the session token from the requests cookies, which come with every request
	c, err := r.Cookie("token")
	if err != nil {
		if err == http.ErrNoCookie {
			// If the cookie is not set, return an unauthorized status
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		// For any other type of error, return a bad request status
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Get the JWT string from the cookie
	tknStr := c.Value

	// Initialize a new instance of `Claims`
	claims := &Claims{}

	// Parse the JWT string and store the result in `claims`.
	// Note that we are passing the key in this method as well. This method will return an error
	// if the token is invalid (if it has expired according to the expiry time we set on sign in),
	// or if the signature does not match
	tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtKey, nil
	})
	if err != nil {
		if err == jwt.ErrSignatureInvalid {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if !tkn.Valid {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}

	// Finally, return the welcome message to the user, along with their
	// username given in the token
	w.Write([]byte(fmt.Sprintf("Welcome %s!", claims.Username)))
}

Renewing your token

In this example, we have set a short expiry time of five minutes. We should not expect the user to login every five minutes if their token expires. To solve this, we will create another /refresh route that takes the previous token (which is still valid), and returns a new token with a renewed expiry time.

To minimize misuse of a JWT, the expiry time is usually kept in the order of a few minutes. Typically the client application would refresh the token in the background.

func Refresh(w http.ResponseWriter, r *http.Request) {
	// (BEGIN) The code uptil this point is the same as the first part of the `Welcome` route
	c, err := r.Cookie("token")
	if err != nil {
		if err == http.ErrNoCookie {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	tknStr := c.Value
	claims := &Claims{}
	tkn, err := jwt.ParseWithClaims(tknStr, claims, func(token *jwt.Token) (interface{}, error) {
		return jwtKey, nil
	})
	if err != nil {
		if err == jwt.ErrSignatureInvalid {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if !tkn.Valid {
		w.WriteHeader(http.StatusUnauthorized)
		return
	}
	// (END) The code up-till this point is the same as the first part of the `Welcome` route

	// We ensure that a new token is not issued until enough time has elapsed
	// In this case, a new token will only be issued if the old token is within
	// 30 seconds of expiry. Otherwise, return a bad request status
	if time.Unix(claims.ExpiresAt, 0).Sub(time.Now()) > 30*time.Second {
		w.WriteHeader(http.StatusBadRequest)
		return
	}

	// Now, create a new token for the current use, with a renewed expiration time
	expirationTime := time.Now().Add(5 * time.Minute)
	claims.ExpiresAt = expirationTime.Unix()
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString(jwtKey)
	if err != nil {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	// Set the new token as the users `token` cookie
	http.SetCookie(w, &http.Cookie{
		Name:    "token",
		Value:   tokenString,
		Expires: expirationTime,
	})
}

Running our application

To run this application, build and run the Go binary:

go build
./jwt-go-example

Now, using any HTTP client with support for cookies (like Postman, or your web browser) make a sign-in request with the appropriate credentials:

POST http://localhost:8000/signin

{"username":"user1","password":"password1"}

You can now try hitting the welcome route from the same client to get the welcome message:

GET http://localhost:8000/welcome

Hit the refresh route, and then inspect the clients cookies to see the new value of the token cookie:

POST http://localhost:8000/refresh

You can find the working source code for this example here.


Like what I write? Join my mailing list, and I'll let you know whenever I write another post

Comments

Soham Kamani

Written by Soham Kamani, an author,and a full-stack developer who has extensive experience in the JavaScript ecosystem, and building large scale applications in Go. He is an open source enthusiast and an avid blogger. You should follow him on Twitter