Password-less Auth in Go – Hands-on Part 1
Tags: webdev, go, redis, twilio
Published: 2024-02-24 10:55:45

Picking up right where we left off, we will start with the packages before we come to the `main` package. This will mean taking a bottom-up approach to coding our Go server. So, without further ado…

## Utility Package
Why are we choosing this one first? For the simple reason that the `utils` package in a Go project contains most of the independent code. Other packages typically use this package. In our `utils` folder inside `pkg` folder, we will create a `utils.go` which will look like the code below:

```go
package utils

import (
	"crypto/rand"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"regexp"
	"time"

	"github.com/go-playground/validator/v10"
	"github.com/golang-jwt/jwt/v5"
	"github.com/joho/godotenv"
	"github.com/sendgrid/sendgrid-go"
	"github.com/sendgrid/sendgrid-go/helpers/mail"
	"github.com/twilio/twilio-go"
	api "github.com/twilio/twilio-go/rest/api/v2010"
	"go.mongodb.org/mongo-driver/bson/primitive"
)

var validate = validator.New()

type GenericJsonResponseDTO struct {
	Message string `json:"message"`
}

// DecodeJSONRequest decodes the JSON request body into the provided interface and validates it.
func DecodeJSONRequest(r *http.Request, v interface{}) error {
	err := json.NewDecoder(r.Body).Decode(v)
	if err != nil {
		return err
	}

	// Validate the decoded struct
	return validate.Struct(v)
}

// EncodeJSONResponse encodes the provided interface as JSON and writes it to the response writer.
func EncodeJSONResponse(w http.ResponseWriter, status int, v interface{}) error {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	return json.NewEncoder(w).Encode(v)
}

func GetENV(name string) string {
	err := godotenv.Load(".env.local")
	if err != nil {
		log.Fatal("ERROR while loading the env file")
		log.Fatal(err)
	}
	return os.Getenv(name)
}

func IsValidPhoneNumber(phoneNo string) bool {
	e164Regex := `^\+[1-9]\d{1,14}$`
	re := regexp.MustCompile(e164Regex)

	return re.Find([]byte(phoneNo)) != nil
}

func GenerateJWT(id string) (string, error) {
	claims := jwt.RegisteredClaims{
		// Also fixed dates can be used for the NumericDate
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 2)),
		Issuer:    GetENV("JWT_ISSUER"),
		Subject:   id,
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte(GetENV("JWT_SECRET")))
}

func ValidateJWT(tokenString string) (jwt.MapClaims, error) {
	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {

		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("Invalid Token Signing Method: %v", token.Header["alg"])
		}

		return []byte(GetENV("JWT_SECRET")), nil
	})
	if err != nil {
		return nil, err
	}

	if !token.Valid {
		return nil, fmt.Errorf("Invalid Token")
	}
	if claims, ok := token.Claims.(jwt.MapClaims); ok {
		return claims, nil
	} else {
		return nil, fmt.Errorf("Invalid Token Claim")
	}

}

func OTPGenerator() (string, error) {
	const otpChars = "0123456789"
	buffer := make([]byte, 8)
	_, err := rand.Read(buffer)
	if err != nil {
		return "", err
	}

	otpCharsLength := len(otpChars)
	for i := 0; i < 8; i++ {
		buffer[i] = otpChars[int(buffer[i])%otpCharsLength]
	}

	return string(buffer), nil
}

func SendOTPMail(receipientEmail string, otp string) error {
	from := mail.NewEmail(GetENV("SENDER_NAME"), GetENV("SENDER_EMAIL")) // Change to your verified sender
	subject := "Your Login OTP"
	to := mail.NewEmail(receipientEmail, receipientEmail)
	plainTextContent := fmt.Sprintf("Your OTP is %s", otp)
	htmlContent := fmt.Sprintf("Your OTP is <strong>%s</string>", otp)
	message := mail.NewSingleEmail(from, subject, to, plainTextContent, htmlContent)
	client := sendgrid.NewSendClient(os.Getenv("SENDGRID_API_KEY"))

	if _, err := client.Send(message); err != nil {
		log.Println("[ERROR]", err)
		return err
	}
	return nil

}

func SendOTPSms(receipientNo string, otp string) error {
	client := twilio.NewRestClient()

	params := &api.CreateMessageParams{}
	params.SetBody(fmt.Sprintf("Your OTP for Login is %s.", otp))
	params.SetFrom(GetENV("TWILLIO_PHONE_NO"))
	params.SetTo(receipientNo)

	_, err := client.Api.CreateMessage(params)
	return err
}

func IsValidObjectID(id string) bool {
	// Use a regular expression to check for the correct format (hexadecimal, 24 characters).
	objectIDRegex := regexp.MustCompile(`^[0-9a-fA-F]{24}$`)
	if !objectIDRegex.MatchString(id) {
		return false
	}

	// Use the MongoDB Go driver to try parsing the string as an ObjectID.
	_, err := primitive.ObjectIDFromHex(id)
	return err == nil
}

```
We will use the Go-Validator module to validate the type of our request body. We initialize the validator at line 23. The `GenericJsonResponseDTO` contains a generic structure of a response body. We can use it by itself or by embedding it inside another DTO as we will see later. The `DecodeJSONRequest()` function takes in the request and then unmarshals the request body into the struct passed in the second argument. Because we are going to use it with a variety of DTOs, we use the open-ended `interface{}` type in Go. This will allow us to pass any struct.

Next, we have the `EncodeJSONRequest()` function. This function takes the `ResponseWriter` interface that is used by a Go HTTP handler to construct an HTTP response, an HTTP response status code, and an interface (typically our response struct DTO) and marshals the data in the struct DTO to the response body.

The `GetENV()` function at line 47 is a wrapper to help us get any environment variable in a single line using the `godotenv` package. The point to note here is that we will pick our environment variables from the `.env.local` file as shown in line 48. After that we have a simple function which validates if a given string is a valid phone number using RegExp.

We need to generate JWT for access control. For this, we will use the [`golang-jwt`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5) module. At line 63, we create a wrapper called `GenerateJWT()` which takes the Mongo DB ID and then creates a JWT. We put the ID into the `subject` field of the JWT. This helps us in getting back the user ID easily using the [`getSubject()`](https://pkg.go.dev/github.com/golang-jwt/jwt/v5#MapClaims.GetSubject) function of `golang-jwt` package. The `GenerateJWT()` function returns a signed JWT and an `error`. If everything is all right, then the `error` is `nil`.

At line 74, we have the `ValidateJWT()` function. This takes a JWT and then decodes the JWT using `jwt.Parse()`. The `jwt.Parse()` takes the encoded JWT token and a function that returns the secret used to sign the JWT. Inside the function, at line 77, we also check if the token is signed using the algorithm of our choice. If a token has expired, then even if the token is decoded for its data, it stands as invalid. At line 87, we check if the token is “valid”. We take out the JWT claims and return them with the error as `nil` at line 91. This completes the happy flow of JWT token validation.

The `OTPGenerator()` function returns a random 8-digit OTP for our use. It’s the `SendOTPMail()` that’s more interesting. It is a wrapper around SendGrid’s Mail SDK. It takes the email of the person to send the email to and the OTP. It then formats a mail and sends it to the recipient. It uses the API key obtained from SendGrid’s Dashboard. You also need to make sure that you have verified sender to complete this step.

> You can start by having a **Single Sender Verification** done for SendGrid account by following the steps listed [here](https://docs.sendgrid.com/ui/sending-email/sender-verification).

The `SendOTPSms()` function uses Twilio’s REST Client for sending an SMS. This means you need to have a registered phone number beforehand on Twilio’s Dashboard. Note how, unlike the Mail SDK, this one does not need any API key. The helpers under the hood consume the API key you have kept in the .env file. It provides a better DevX.

Lastly, the `IsValidObjectID()` takes in a string and then returns if the string is a valid MongoDB object ID. This is used for request and JWT-related validation purposes. Because the subject of our JWT will have the user ID, this utility function will help us validate the JWT in more ways than one.

## Config Package
The `config` package in our Golang web server, as you might have guessed, will contain the configuration files. More specifically, database connections and contexts will be exported from this file. But it won’t function quite the same way as you might have guessed if you are coming from another language. The code given below sums up what we will have in our `app.go` in our `config` package:

```go
package config

import (
	"context"
	"fmt"
	"log"

	"github.com/abhik-99/passwordless-login/pkg/utils"

	"github.com/go-redis/redis/v8"
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
	"go.mongodb.org/mongo-driver/mongo/readpref"
)

var (
	Db       *mongo.Database
	MongoCtx context.Context

	client *mongo.Client

	Rdb      *redis.Client
	RedisCtx context.Context
)

func init() {
	MongoCtx = context.Background()
	client, err := mongo.Connect(MongoCtx, options.Client().ApplyURI(fmt.Sprintf("mongodb://%s:%s@localhost:27017/?retryWrites=true&w=majority", utils.GetENV("DBUSER"), utils.GetENV("DBPASS"))))
	if err != nil {
		// panic(err)
		log.Println(err)
	}

	if err = client.Ping(MongoCtx, readpref.Primary()); err == nil {
		log.Print("Connection to DB Successful")
	} else {
		log.Println("ERROR while pinging DB")
		log.Panic(err)
		return
	}

	Db = client.Database("passwordless-auth")
	Db.CreateCollection(MongoCtx, "user-collection")

	Rdb = redis.NewClient(&redis.Options{
		Addr:     utils.GetENV("REDISADDR"),
		Password: utils.GetENV("REDISPASS"),
		DB:       0, // use default DB
	})

	RedisCtx = context.Background()
}

func Disconnect() {
	client.Disconnect(MongoCtx)

	Rdb.Close()
}


```
Between lines 16 and 24, we define the variables we mean to export to other packages. As is apparent, we are exposing the `Db` and `MongoCtx` variables. The former is the connection to our specific Mongo database (which we will call `passwordless-auth`). We are not going to export our Mongo Client though. The last two variables are connection to our Redis Database instance and the respective context.

> Notice how we are using the `GetENV()` function from the `utility` package to get the DB username and password for the connection string.

Next, we have a special function called `init()`. What makes the `init()` function special in Go? Well, the `init()` function in Golang is a lifecycle function which is called even before the `main()` function is called. It initializes the application. You can have an `init()` function in every package and be assured that those will be executed even before the `main()` function is. This assures that some states are set before the app runs.

Inside our `init()` function, we first create a `context` and assign it to `MongoCtx`. We then create a client with the connection URL which helps us connect to our Docker Container where Mongo is running (refer to the previous article in the series). At line 42, we create a pointer to the `passwordless-auth` database. *A new DB will not be created in this case until we create a collection in it*. So, we create a MongoDB collection called `user-collection`. 

The `createCollection()` function will create our collection if it does not exist. Otherwise, it will quietly send an error. We won’t concern ourselves with the error in this case as this step ensures that our collection is created before we start using it and we don’t end up with a null pointer error.

Lastly, we initialize the connection to our Redis instance running in a Docker container (again, refer to the previous article in the series). The Rdb and RedisCtx are Redis equivalents of the Mongo connection and context.

The `Disconnect()` function being exported here is going to be invoked when the application is stopped. It is meant to close all the connections and gracefully shut down our application. It will be used in the `main` package.

## Main Package
At this point, we have crossed the 1k-word limit. So, I am a bit wary. But I feel that this article will not be complete without a discussion of the `main` package. If you are new to Go, the `main` package is meant to house our `main()` function which is run right after any `init()` function by Go when the application starts execution. In our case, we will keep the `main.go` inside the `cmd` folder.

```go
package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/abhik-99/passwordless-login/pkg/config"
	"github.com/abhik-99/passwordless-login/pkg/routes"

	"github.com/gorilla/mux"
)

func main() {
	defer config.Disconnect()
	router := mux.NewRouter()
	router.StrictSlash(true)

	authRouter := router.PathPrefix("/auth").Subrouter()
	routes.RegisterAuthRoutes(authRouter)

	userRouter := router.PathPrefix("/user").Subrouter()
	routes.RegisterUserRoutes(userRouter)

	fmt.Println("Started on PORT 3000")
	http.Handle("/", router)

	log.Fatal(http.ListenAndServe(":3000", router))
}

```

As shown in the code above, the first thing we do is start by **deferring** the execution of the `Disconnect()` function which we defined in the `config` package in the previous section. This is convenient as it ensures “we don’t forget about closing DB connections”. 

Next, we create our main router. We set `StrictSlash()` to `true` on our router. To understand the effect of this with an example, this means that the paths trailing with `/user` and `/user/` are evaluated to the same controller.

At line 19, we create our authentication subrouter called `authRouter`. Any routes registered to this router will have the prefix `/auth` in them. We then pass this Gorilla Mux subrouter to the `RegisterAuthRoutes()` function in the `routes` package.

> We are yet to define the `routes` and `controllers` package.

Similarly, we create our user subrouter and pass it to `RegisterUserRoutes()` function which we will define in the next article. This shows the finesse of Gorilla Mux. It allows easy grouping of routes through this system. One can similarly group our protected and public routes in their project when using Gorilla Mux.

After this, we use the `net/http` module from the Standard Go library to start listening with our Gorilla Mux router which we created in line 16. This completes three of the packages of our application.

## Conclusion
In this article, we took a hands-on approach to writing our Go web server in Gorilla Mux. I am way past the 1k-ish word limit so I am going to end this article at this point. In the next articles in this series, we will define our controllers and models for our specific routes. Until then, keep building awesome things and WAGMI!