Blog Migration VII : Securing API: JWT and HTTPS/TLS

In today's post, I will delve into security side of my app. I am using JWT Token to authorize API access to write, update, and delete a post. Also, I configured HTTPS/TLS to ensure secure data transmission.

.
└── src
  ├── db
  └── server
    ├── auth.go
    └── server.go

JWT Authentication

Implementiong JWT Authenticaiton

Let's take a closer look at the auth.go code that handles JWT token generation and validation. I am using golang-jwt package to generate jwt token and godotenv for loading secret from environment variable.

First, I set JWT secret and API key in the .env file located at the root of the package. JWT secret will be used for generating jwt token, and api key is used for authenticating the /auth endpoint which returns JWT token to client when correct key is given.

JWT_SECRET = 
API_KEY = 

Next, I loaded the secret like this.

func LoadSecret() {
		err := godotenv.Load()
    if err != nil {
        log.Fatalf("err loading: %v", err)
    }

	SECRET = []byte(os.Getenv("JWT_SECRET"))
	API_KEY = os.Getenv("API_KEY")
}

I then used this secret to generate the token as shown below.

func generateJwt() (string, error) {
	token := jwt.New(jwt.SigningMethodHS256)
	claims := token.Claims.(jwt.MapClaims)
	claims["exp"] = time.Now().Add(time.Hour * 12).Unix()
	tokenStr, err := token.SignedString(SECRET)

	if err != nil {
		log.Printf(err.Error())
		return "", err
	}

	return tokenStr, nil
}

Additionally, token validation is handled like so.

func validateJwt(next func(w http.ResponseWriter, r *http.Request)) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if r.Header["Token"] != nil {
			token, err := jwt.Parse(r.Header.Get("Token"), func(token *jwt.Token) (interface{}, error) {
				_, ok := token.Method.(*jwt.SigningMethodHMAC)
				if !ok {
					return nil, errors.New("not authorized")
				}
				return SECRET, nil
			})

			if err != nil {
				w.WriteHeader(http.StatusUnauthorized)
				w.Write([]byte("not authorized: " + err.Error()))
			}

			if token.Valid {
				next(w, r)
			}
		} else {
			w.WriteHeader(http.StatusUnauthorized)
			w.Write([]byte("not authorized"))
		}
	})
}

Note that validateJwt() is a middleware. It is used in either of these ways:

http.Handle("/admin", validateJwt(admin))

//or inside a handler function
validateJwt(admin).Serve(w, r)

Finally, /auth endpoint is handled like this:

func getJwt(w http.ResponseWriter, r *http.Request) {
	...
	//verify whether given JWT token is valid. this does not delete token
	if r.Method == http.MethodDelete {
		validateJwt(func(w http.ResponseWriter, r *http.Request) {
			w.Write([]byte("authorized"))
		}).ServeHTTP(w,r)
		return
	}

	//verify api key and return token
	_, ok := r.Header["Api-Key"]
	if ok && r.Header["Api-Key"][0] == API_KEY {
		token, err := generateJwt()
		if err != nil {
			w.WriteHeader(http.StatusUnauthorized)
			w.Write([]byte(err.Error()))
			return
		}
		w.Write([]byte(token))
	} else {
		w.WriteHeader(http.StatusUnauthorized)
		w.Write([]byte("not authorized"))
	}
}

On client-side, JWT token is retrieved and stored in cookie:

await fetch(`https://107106.xyz/auth`, {
            method: 'GET',
            headers: {
                'Api-Key': input_element!.value
            }
        }).then(response => response.status == 401 ? null : response.text())
        .then(tok => {
            if (tok != null) {
                window.alert('you are authenticated');
                document.cookie = 'token='+tok;
                // ...
            } else {
                window.alert('invalid key');
            }
        })

HTTPS/TLS

Domain and SSL Certificate

Securing the transmission of data between clients and the server is a cornerstone of application security. This is where HTTPS/TLS comes into play. In order to setup HTTPS/TLS, I needed to get a domain and SSL certificate first.

After researching, I discovered that .xyz domains with 6-9 digits cost less than a dollar annually (link). Since this domain will only be used for my backend API, I did not need an easily recognizable one. So, I purchased mine at Porkbun for $1.22/year, including the SSL certificate.

Setting Up DNS

Now, I need my domain to point to the GCP instance running this my Go server. I added a DNS record through the Porkbun domain management console.

dns-record

Enabling HTTPS/TLS

Lastly, I utilized the SSL certificate purchased from PorkBun to enable TLS.

const (
	// ...
	CERTFILEPATH = 
	KEYFILEPATH  = 
)

// Run initializes the server and handles routes
func Run() {
	// ...
	if err := http.ListenAndServeTLS(":443", CERTFILEPATH, KEYFILEPATH, nil); err != nil {
		log.Fatal(err)
	}
}

Enabling HTTPS/TLS guarantees that the data transmitted between the client and server remains confidential and tamper-proof, thwarting potential eavesdropping and data manipulation attacks.

Links