Video:

JWT Authentication

February 28, 2021

Course Instructor: Elliot Forbes

Hey Gophers! My name is Elliot and I'm the creator of TutorialEdge and I've been working with Go systems for roughly 5 years now.

In the last video, we looked at how we could wrap some of our endpoints with basic HTTP authentication. In this video, we are going to take this one step further and look at implementing JWT authentication.

We are going to be following the same approach that we took in the previous video and creating a decorator function that will effectively act like middleware around the endpoints we want to protect with JWT authentication.

Implementation

Let’s dive into our handler.go file and start fleshing out this new decorator function:

// JWTAuth - a handy middleware function that will provide basic auth around specific endpoints
func JWTAuth(original func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		log.Info("jwt auth endpoint hit")
		
		if true {
			original(w, r)
		} else {
			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
			sendErrorResponse(w, "not authorized", errors.New("not authorized"))
		}
	}
}

So, at the moment we have a placeholder for our JWT Authentication that will just allow us to pass through to the underlying endpoint. This obviously isn’t secure so let’s start implementing the JWT validation aspect of this decorator now:

$ go get github.com/dgrijalva/jwt-go

Next, we want a signing key to validate that the incoming. At this point, you have the option to generate RSA keys and read them in from within your application container, or you can use a secret string that you pull in from your underlying environment variables. We are going to use this approach for now, but you should be able to swap this out for a more secure cert-based approach very quickly if need be.

// validateToken - validates an incoming jwt token
func validateToken(accessToken string) bool {
	// replace this by loading in a private RSA cert for more security
	var mySigningKey = []byte("missionimpossible")
	token, err := jwt.Parse(accessToken, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("There was an error")
		}
		return mySigningKey, nil
	})

	if err != nil {
		return false
	}

	return token.Valid
}

Now that we have this handy private function in place, let’s update our decorator JWTAuth function:

// JWTAuth - a handy middleware function that will provide basic auth around specific endpoints
func JWTAuth(original func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
	return func(w http.ResponseWriter, r *http.Request) {
		log.Info("jwt auth endpoint hit")
		authHeader := r.Header["Authorization"]
		if authHeader == nil {
			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
			sendErrorResponse(w, "not authorized", errors.New("not authorized"))
		}

		authHeaderParts := strings.Split(authHeader[0], " ")
		if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
			sendErrorResponse(w, "not authorized", errors.New("not authorized"))
		}

		if validateToken(authHeaderParts[1]) {
			original(w, r)
		} else {
			w.Header().Set("Content-Type", "application/json; charset=UTF-8")
			sendErrorResponse(w, "not authorized", errors.New("not authorized"))
		}
	}
}

And perfect! We now have a decorator function that we can wrap around endpoints and further improve the security of our application:

// SetupRoutes - sets up all the routes for our application
func (h *Handler) SetupRoutes() {
	log.Info("Setting Up Routes")
	h.Router = mux.NewRouter()
	h.Router.Use(LoggingMiddleware)

	h.Router.HandleFunc("/api/comment", h.GetAllComments).Methods("GET")
	h.Router.HandleFunc("/api/comment", JWTAuth(h.PostComment)).Methods("POST")
	h.Router.HandleFunc("/api/comment/{id}", h.GetComment).Methods("GET")
	h.Router.HandleFunc("/api/comment/{id}", JWTAuth(h.UpdateComment)).Methods("PUT")
	h.Router.HandleFunc("/api/comment/{id}", JWTAuth(h.DeleteComment)).Methods("DELETE")

	h.Router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json; charset=UTF-8")
		w.WriteHeader(http.StatusOK)
		if err := json.NewEncoder(w).Encode(Response{Message: "I am Alive!"}); err != nil {
			panic(err)
		}
	})
}

Let’s try and run this now:

$ docker-compose up --build

When we go to hit the endpoints, we should see that the endpoints and now protected by JWT authentication and can only be accessed if you pass in an Authorization header that has a token that has been signed within our secret.