Build an Authentication Microservice in Golang from the Scratch

How to Build an Authentication Microservice in Golang from Scratch

Golang is one of the leading tools to build microservices. In a cloud-native application, a microservice is a loosely coupled application that provides important functionality. This article explores the steps you need to take to set up an authentication microservice for your application. Most of the functionality has been implemented from scratch to give you a better understanding of how Golang works.

There are many libraries — including jwt-go — available for the implementation of JSON Web Tokens (JWT). However, this guide explores implementation using your own logic to generate and validate web tokens. Additionally, the guide also covers how to handle user data, how to implement middleware the main server will use, and how to make handler functions. The guide also focuses on routing using the Gorilla MUX framework to ensure the application is accessible to the public using the REST framework. 

Finally, the guide teaches you how to containerize the microservice using Docker. This will make sure you can deploy the microservice in a continuous integration/continuous deployment pipeline (CI/CD) to an orchestration service like Kubernetes.

At a high level, here are the parts of the project you’ll learn how to build by the end of this tutorial:

1. Handle User Data and Provide Query Functionality

2. Generate and Validate User Session Tokens

3. Authentication and Token Validation Middleware

4. Add MUX Routing Support

5. Containerize the Application With Docker

Handle User Data and Provide Query Functionality

To start designing the authentication microservice, you first need to think about what the user schema will look like and what schema functions should be exposed to other packages. You’ll want to make sure that unnecessary data isn’t exposed to other modules and that only the required data is available. So, you’ll need to define the private and public functions accordingly:

type user struct {
 email string
 username string
 passwordhash string
 fullname string
 createDate string
 role int
}

You’ll want to implement a multi-level authentication system, which is defined in the role field. If the role field is 0, the user is a standard user; if not, the user is an admin. 

Based on the role, you can define the authentication levels and their priority. This idea can be extended further to develop more complex microservices. In addition to the role, a user needs to have a unique email and username, password hash, full name, and the date the account was created. The microservice expects the password to be already hashed from the client itself. So, the server will never know what the password of a user is. In case of a security breach, only the hashed values of the password will be revealed.

From the outset, users are stored in a list. However, this can be changed by attaching a database. To provide functionality for this data, you shouldn’t export the whole user list outside this package since you want to keep the module loosely coupled. Instead, you’ll use these methods: GetUserObject, ValidatePasswordHash, which is a method of struct that does a simple string comparison, and AddUserObject, which verifies whether the username or email already exists in the database and, if not, adds the user.

func GetUserObject(email string) (user, bool) {
	//needs to be replaces using Database
	for _, user := range userList {
		if user.email == email {
			return user, true
		}
	}
	return user{}, false
}

// checks if the password hash is valid
func (u *user) ValidatePasswordHash(pswdhash string) bool {
	return u.passwordhash == pswdhash
}

// this simply adds the user to the list
func AddUserObject(email string, username string, passwordhash string, fullname string, role int) bool {
	// declare the new user object
	newUser := user{
		email:        email,
		passwordhash: passwordhash,
		username:     username,
		fullname:     fullname,
		role:         role,
	}
	// check if a user already exists
	for _, ele := range userList {
		if ele.email == email || ele.username == username {
			return false
		}
	}
	userList = append(userList, newUser)
	return true
}

Generate and Validate User Session Tokens

A web token is a dot-separated string consisting mainly of three parts: a header, a payload, and a signature. Here, the header and payload are simple base64 encoded strings that can be decoded easily. That being the case, it’s important to ensure you don’t write anything in them that might lead to the disclosure of sensitive information. 

The signature is the main part used to validate the tokens. It’s encrypted using SHA256 or some other algorithm with a private key known only to the server.

The private key is the main breaking point of your authentication microservice. If this is compromised, anyone can replicate the token and manipulate the server. As such, you must pass it as an environment variable and ensure it’s handled with the utmost care. The key should be random and extremely complex. I have passed it as a string inside the code in this example, but we should pass it as an environment variable to a Docker container.

The server generates the web tokens and validates them. So, you should pass some parameters in the payload in the server, including an issuer (e.g., the domain name of your website), an audience (e.g., the client for whom the token is meant), and the expiration date. While the first two parameters can help you ensure the authentication is coming from a trusted source, the last parameter ensures tokens stay secure by regenerating them periodically.

To generate a token, use Base64 to encode the header and the payload, which in this case is of type map[string]string. To generate the signature, you need to encrypt the header and payload together using a secret key. To validate the token, decode the header and the payload strings and then combine them to create a new signature. If this matches the signature present in the token, the token is valid. You can also set an expiration check here that indicates when the token will expire.

// Function for generating the tokens.
func GenerateToken(header string, payload map[string]string, secret string) (string, error) {
	// create a new hash of type sha256. We pass the secret key to it
	h := hmac.New(sha256.New, []byte(secret))
	header64 := base64.StdEncoding.EncodeToString([]byte(header))
	// We then Marshal the payload which is a map. This converts it to a string of JSON.
	payloadstr, err := json.Marshal(payload)
	if err != nil {
		fmt.Println("Error generating Token")
		return string(payloadstr), err
	}
	payload64 := base64.StdEncoding.EncodeToString(payloadstr)

	// Now add the encoded string.
	message := header64 + "." + payload64

	// We have the unsigned message ready.
	unsignedStr := header + string(payloadstr)

	// We write this to the SHA256 to hash it.
	h.Write([]byte(unsignedStr))
	signature := base64.StdEncoding.EncodeToString(h.Sum(nil))

	//Finally we have the token
	tokenStr := message + "." + signature
	return tokenStr, nil
}

// This helps in validating the token
func ValidateToken(token string, secret string) (bool, error) {
	// JWT has 3 parts separated by '.'
	splitToken := strings.Split(token, ".")
	// if length is not 3, we know that the token is corrupt
	if len(splitToken) != 3 {
		return false, nil
	}

	// decode the header and payload back to strings
	header, err := base64.StdEncoding.DecodeString(splitToken[0])
	if err != nil {
		return false, err
	}
	payload, err := base64.StdEncoding.DecodeString(splitToken[1])
	if err != nil {
		return false, err
	}
	//again create the signature
	unsignedStr := string(header) + string(payload)
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(unsignedStr))

	signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
	fmt.Println(signature)

	// if both the signature don’t match, this means token is wrong
	if signature != splitToken[2] {
		return false, nil
	}
	// This means the token matches
	return true, nil
}

Authentication and Token Validation Middleware

Let’s start with the simple SignUp handler. A handler is a function that performs operations when a certain route is called by the router. If the router detects the request to a route and you’ve assigned a function to it, this handler — which takes a ResponseWriter as a parameter — is called.

The SignUp handler takes the parameters out of a request and also checks if the request has all the necessary parameters. If even one of the parameters is missing, an error code will be written to the client. Next, it calls the AddUserObject method from the data package and checks whether the user has been added. There’s only one case when a user won’t be added: when a duplicate email or username is provided.

// adds the user to the database of users
func SignupHandler(rw http.ResponseWriter, r *http.Request) {
	// extra error handling should be done at server side to prevent malicious attacks
	if _, ok := r.Header["Email"]; !ok {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Email Missing"))
		return
	}
	if _, ok := r.Header["Username"]; !ok {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Username Missing"))
		return
	}
	if _, ok := r.Header["Passwordhash"]; !ok {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Passwordhash Missing"))
		return
	}
	if _, ok := r.Header["Fullname"]; !ok {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Fullname Missing"))
		return
	}

	// validate and then add the user
	check := data.AddUserObject(r.Header["Email"][0], r.Header["Username"][0], r.Header["Passwordhash"][0],
		r.Header["Fullname"][0], 0)
	// if false means username already exists
	if !check {
		rw.WriteHeader(http.StatusConflict)
		rw.Write([]byte("Email or Username already exists"))
		return
	}
	rw.WriteHeader(http.StatusOK)
	rw.Write([]byte("User Created"))
}

Next, implement a SignIn handler, which just needs two parameters: Email and Passwordhash. If the user exists, they will be validated using the ValidateHash method of the user structure. After this, the token will be generated and sent back to the client, which will then send the token with every authentication request.

// we need this function to be private
func getSignedToken() (string, error) {
	// we make a JWT Token here with signing method of ES256 and claims.
	// claims are attributes.
	// aud - audience
	// iss - issuer
	// exp - expiration of the Token
	claimsMap := map[string]string{
		"aud": "frontend.knowsearch.ml",
		"iss": "knowsearch.ml",
		"exp": fmt.Sprint(time.Now().Add(time.Minute * 1).Unix()),
	}
	// here we provide the shared secret. It should be very complex.
	// Also, it should be passed as a System Environment variable

	secret := "Secure_Random_String"
	header := "HS256"
	tokenString, err := jwt.GenerateToken(header, claimsMap, secret)
	if err != nil {
		return tokenString, err
	}
	return tokenString, nil
}

// searches the user in the database.
func validateUser(email string, passwordHash string) (bool, error) {
	usr, exists := data.GetUserObject(email)
	if !exists {
		return false, errors.New("user does not exist")
	}
	passwordCheck := usr.ValidatePasswordHash(passwordHash)

	if !passwordCheck {
		return false, nil
	}
	return true, nil
}

func SigninHandler(rw http.ResponseWriter, r *http.Request) {
	// validate the request first.
	if _, ok := r.Header["Email"]; !ok {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Email Missing"))
		return
	}
	if _, ok := r.Header["Passwordhash"]; !ok {
		rw.WriteHeader(http.StatusBadRequest)
		rw.Write([]byte("Passwordhash Missing"))
		return
	}
	// let’s see if the user exists
	valid, err := validateUser(r.Header["Email"][0], r.Header["Passwordhash"][0])
	if err != nil {
		// this means either the user does not exist
		rw.WriteHeader(http.StatusUnauthorized)
		rw.Write([]byte("User Does not Exist"))
		return
	}

	if !valid {
		// this means the password is wrong
		rw.WriteHeader(http.StatusUnauthorized)
		rw.Write([]byte("Incorrect Password"))
		return
	}
	tokenString, err := getSignedToken()
	if err != nil {
		fmt.Println(err)
		rw.WriteHeader(http.StatusInternalServerError)
		rw.Write([]byte("Internal Server Error"))
		return
	}

	rw.WriteHeader(http.StatusOK)
	rw.Write([]byte(tokenString))
}

Finally, you need to implement middleware, which returns a handler function. It’s called each time a request reaches a router. The main focus of authentication middleware is to ensure no request is fulfilled without validating the web token. If the token is invalid, the client will have to regenerate it, which protects against distributed denial of service (DDoS) attacks.

If the token is valid, send an “authorization confirmed” message with a 200 OK status code. If not, you’ll know that the incoming request is unauthorized and can send the response code accordingly.

// We want all our routes for REST to be authenticated. So, we validate the token
func tokenValidationMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
		// check if token is present
		if _, ok := r.Header["Token"]; !ok {
			rw.WriteHeader(http.StatusUnauthorized)
			rw.Write([]byte("Token Missing"))
			return
		}
		token := r.Header["Token"][0]
		check, err := jwt.ValidateToken(token, "Secure_Random_String")

		if err != nil {
			rw.WriteHeader(http.StatusInternalServerError)
			rw.Write([]byte("Token Validation Failed"))
			return
		}
		if !check {
			rw.WriteHeader(http.StatusUnauthorized)
			rw.Write([]byte("Token Invalid"))
			return
		}
		rw.WriteHeader(http.StatusOK)
		rw.Write([]byte("Authorized Token"))

	})
}

Add MUX Routing Support

At this point, you have your routing handlers and middleware ready. Now, you need to provide these to a router. 

In this tutorial, Gorilla’s MUX framework is used for routing because it’s an easy and fast tool for this purpose (you can also use alternatives like gin). In this guide, the signup and signin field have been defined. This means they will be called by routes as SubRouter in the routing. /auth/signin also pass middlewares to different routes using and /auth/signup/. We can router.Use().

package main

import (
	"fmt"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/shadowshot-x/micro-product-go/authservice"
)

func main() {
	mainRouter := mux.NewRouter()
	authRouter := mainRouter.PathPrefix("/auth").Subrouter()
	authRouter.HandleFunc("/signup", authservice.SignupHandler)

	// The Signin will send the JWT back as we are making microservices.
	// The JWT token will make sure that other services are protected.
	// So, ultimately, we would need a middleware
	authRouter.HandleFunc("/signin", authservice.SigninHandler)

	// Add the middleware to different subrouter
	// HTTP server
	// Add time outs
	server := &http.Server{
		Addr:    "127.0.0.1:9090",
		Handler: mainRouter,
	}
	err := server.ListenAndServe()
	if err != nil {
		fmt.Println("Error Booting the Server")
	}
}

Containerize the Application With Docker

Finally, it’s time to containerize the application you’ve been building using Docker. 

Containers are easy to manage and can be deployed to highly scalable and available platforms like Kubernetes. This is an integral part of the continuous integration pipelines which often use hosted repositories like DockerHub to deploy the images of the application. 

This is a very simple Dockerfile that’s also quite small because you’ve used very few external packages. Here, you can use an Alpine base image with Golang installed to be the base of the container — which you should be able to see as the current machine you’re working on. Now, it’s time to declare the working directory, which is inside the Alpine image. Moving forward, every operation will be in the server directory in the Alpine image. So, copy everything inside your current machine to the Alpine image. 

Once that’s done, install the dependencies like gorilla/mux and then build the application’s binary, which will be run at the end of the Dockerfile. You’ll also need to expose a port, i.e., make the port available to be accessed from outside the container. 

Now it’s time to run the created binary:

# syntax=docker/dockerfile:1
FROM golang:1.16-alpine
WORKDIR /server
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY ./ ./
RUN go build -o /product-go-micro
EXPOSE 9090
CMD [ "/product-go-micro" ]

Build the image:

docker build . -t product-go-micro

Then run it:

docker run --network host -d <--your image id-->

Now, the server is up and running in a Docker container. In this guide, the network is set to be that of the host, which you can see in the Docker run. This means you can now reach the application using the localhost; check the application using the following curl command.

curl https://localhost:9090/auth/signin --header 'Email:[email protected]'
--header 'Passwordhash:hashedme1'

Continue Building

Hopefully this code-along provided you with some insights into how authentication with web tokens works. Building applications with microservices provides an understanding of how routing works and the power of REST APIs. To further integrate the CI/CD process, you can use other tools — like TravisCI and CircleCI.

All of the code found in this blog post can be found on GitHub, or you can download it directly here.
The content of this blog post is published under a CC BY-NC-SA 4.0 license.

Read more about:

golang

Cloud Engineer @ Google India | Ex Intern @ Intuit | Writer | Developer