# Authentication with email and code verification

When using passwords to authenticate our applications, we often want an extra layer of security to prevent malicious attackers from spamming our registration. As such we can try to implement an email and code verification in our app.

In the app that we are creating today, we will be using NextJs for our frontend, Go for our backend and saving our user data into MongoDB.

The full code can be found on my [GitHub repository](https://github.com/AndreWongZH/auth_boilerplate).

## Prerequisites

Before diving into the implementation, make sure to have the following prerequisites:

1. Go installed on your machine
    
2. Node.js and NPM installed on your machine
    
3. MongoDB operations in Go (can read the [documentation here](https://www.mongodb.com/docs/drivers/go/current/quick-start/))
    
4. A MongoDB database set up and accessible (I am using MongoDB Atlas which is exposed using a MONGODB\_URI)
    
5. An email service provider (here I am using Gmail to send my emails)
    

## Architecture

Here is a general overview of the components we will be building and the connection between the different services. We will only be sending our crafted emails to our mail service server using SMTP protocol, which will then forward this email to the intended recipient.

![software architecture](https://cdn.hashnode.com/res/hashnode/image/upload/v1684295358613/ec6c1667-7599-4c27-ae38-81bf6eb46627.png align="center")

## Approach

After a user registers for an account, we want to store their details in our database and then send them an email with a verification link and code.

For our approach to verification with a link, we want to generate a link such that when clicked, will make a GET request to our server and once verified, we will redirect them back to our frontend homepage for them to log in.

For our approach to verification with code, we want to generate a random 4-digit code that is sent to their email. The user can then key this code in the frontend after they registered and if the code is correct, will allow the user into the dashboard.

## Configure SMTP with mail service

The email service provider I am using is Gmail and they have [a guide](https://support.google.com/mail/answer/7126229) on how to enable SMTP on the Gmail account. You can use other options like Sendgrid, Mailgun or Outlook.

For Gmail, we need to enable the **IMAP Access** option on our Gmail settings page.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684305134286/b87c56b1-0c1e-4777-b37c-7ae5ad5a55c7.png align="center")

We will also need to generate an **App Password** as we do not want to use the same password we use for our Google account to authenticate our SMTP access. For that, we can go to Google account settings, under **Security** and then **2 steps verification**, add your new app password.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684305250285/440d3ccf-c08d-4467-873e-926fb6c77dca.png align="center")

## Setting up backend service

1. Make backend folder
    
    ```bash
    mkdir backend
    cd backend
    ```
    
2. Set up a new Go module and install necessary dependencies
    
    ```bash
    go mod init auth-boilerplate
    
    go get github.com/gin-gonic/gin
    go get github.com/gin-contrib/cors
    
    go get go.mongodb.org/mongo-driver/mongo
    go get github.com/joho/godotenv
    
    go get golang.org/x/crypto/bcrypt
    ```
    
3. Set up the API server using `gin` and include `cors` settings so that we do not run into any "Blocked by CORS policy" errors.
    
    ```go
    func main() {
    	r := gin.Default()
    
    	r.Use(cors.New(cors.Config{
    		AllowOrigins:  []string{fmt.Sprintf("http://%s", origin)},
    		AllowMethods:  []string{"POST", "GET", "PUT", "OPTIONS", "DELETE"},
    		AllowHeaders:  []string{"Origin", "Content-Type", "Set-Cookie"},
    		ExposeHeaders: []string{"Content-Length", "Content-Type", "Set-Cookie", "Access-Control-Allow-Credentials", "Access-Control-Expose-Headers", "Access-Control-Allow-Origin", "set-cookie"},
    
    		AllowCredentials: true,
    		AllowWebSockets:  true,
    		MaxAge:           12 * time.Hour,
    	}))
    
    	r.POST("/verify", verifyCode)
    	r.GET("/verify/link", verifyLink)
    	r.POST("/register", register)
    	r.POST("/login", login)
    
    	r.Run(":3001")
    }
    ```
    
4. Establish a connection with the Mongo database using `mongo.Connect()` and if successful, we can use this Mongo client to query and insert our collections. We will use `godotenv.Load(".env")` to load our `MONGODB_URI` value from a `.env` file in our project folder.
    
    ```go
    var client *mongo.Client
    
    func initDatabase() error {
    	uri := os.Getenv("MONGODB_URI")
    	fmt.Println(uri)
    	if uri == "" {
    		log.Println("URI not found in environmental variables")
    		return errors.New("URI not found")
    	}
    
    	var err error
    	client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
    	if err != nil {
    		log.Println("Error connecting to database")
    		return errors.New("cannot connect to db")
    	}
    
    	return nil
    }
    
    func main() {
        err := godotenv.Load(".env")
    	if err != nil {
    		log.Println("failed to load .env file")
    		return
    	}
    	err = initDatabase()
    	if err != nil {
    		return
    	}
        ///...
    }
    ```
    
5. Declare user struct to handle user inputs from JSON and BSON. BSON, which stands for binary JSON, is used by MongoDB to store and transmit data. When `omitempty` is used in the ID field, it means that the ID field will not be added into MongoDB if the value of ID in our User struct is empty, MongoDB will auto-generate the `_id` field for us.
    
    We will be generating the `VerifyHash` and `VerifyCode` on Registration and compare them when they try to verify.
    
    `ValidHash` and `ValidCode` will be storing the deadline when the hash or code is considered invalid and will need to be regenerated.
    
    ```go
    type User struct {
    	ID          string    `bson:"_id,omitempty"`
    	Name        string    `json:"name" bson:"name"`
    	Email       string    `json:"email" bson:"email"`
    	Password    string    `json:"password" bson:"passwordHash"`
    	VerifyHash  string    `bson:"verifyhash"`
    	VerifyCode  string    `bson:"verifycode"`
    	IsVerified  bool      `json:"isVerified" bson:"isVerified"`
    	DateCreated time.Time `bson:"dateCreated"`
    	ValidHash   time.Time `bson:"validHash"`
    	ValidCode   time.Time `bson:"validCode"`
    }
    ```
    
6. Let's add our register function whenever the user registers on the frontend.
    
    `ctx.BindJSON` is used to bind the JSON data from the request body to the `newUser` struct.
    
    `bcrypt.GenerateFromPassword` function is used to generate a secure hash from the user's password. We then replace the current password provided by the user.
    
    We will then generate the verification link and code and add them to the struct, together with their expiration.
    
    We will insert our `newUser` struct into the database using the `InsertOne` method and add it to our `auth` database and `users` collection.
    
    Lastly, we will send the user their verification email with the link and code attached.
    
    ```go
    func register(ctx *gin.Context) {
    	var newUser User
    
    	err := ctx.BindJSON(&newUser)
    	if err != nil {
    		log.Println("Error binding JSON")
    		ctx.AbortWithStatusJSON(http.StatusInternalServerError, nil)
    		return
    	}
    
    	// ensure email does not exist in database
    	// ensure email and password is valid
    	// left as an exercise
    
    	// generate hash from user password
    	hash, err := bcrypt.GenerateFromPassword([]byte(newUser.Password), 6)
    	if err != nil {
    		log.Println("error hashing password")
    	}
    	newUser.Password = string(hash)
    
    	// generate verification link
    	newUser.VerifyHash = generateVerifyHash()
    	// generate verification code
    	newUser.VerifyCode = generateVerifyCode()
    
    	// add deadline for hash and code expiry
    	// in the example here, we use 15mins for hash and 3 mins for code
    	now := time.Now()
    	newUser.DateCreated = now
    	newUser.ValidHash = now.Add(15 * time.Minute)
    	newUser.ValidCode = now.Add(3 * time.Minute)
    
    	// add user to database
    	coll := client.Database("auth").Collection("users")
    	log.Println("add: ", newUser)
    	result, err := coll.InsertOne(context.TODO(), newUser)
    	if err != nil {
    		log.Println("failed to add new user:", err)
    		ctx.JSON(http.StatusOK, gin.H{
    			"success": false,
    		})
    		return
    	}
    
    	fmt.Println(result)
    
    	// send the verification email
    	sendVerificationEmail(newUser)
    
    	ctx.JSON(http.StatusOK, gin.H{
    		"success": true,
    	})
    }
    ```
    
7. Create our login function.
    
    We will use the user's email to query the database and fetch their document.
    
    Once found, use `bcrypt.CompareHashAndPassword` to compare the hash in the database and the user's password in the request body. If successful, then proceed to return a JSON response based on whether the user is verified. If not successful, then return a JSON response with an HTTP code unauthorized.
    
    ```go
    func login(ctx *gin.Context) {
    	var user User
    
    	err := ctx.BindJSON(&user)
    	if err != nil {
    		log.Println("Error binding JSON")
    		ctx.AbortWithStatusJSON(http.StatusInternalServerError, nil)
    		return
    	}
    
    	var dbUser User
    	coll := client.Database("auth").Collection("users")
    	filter := bson.D{{"email", user.Email}}
    	err = coll.FindOne(context.TODO(), filter).Decode(&dbUser)
    	if err != nil {
    		if err == mongo.ErrNoDocuments {
    			log.Println(fmt.Sprintf("searching for users with email: %s yield no result found", dbUser.Email))
    			ctx.JSON(http.StatusUnauthorized, gin.H{
    				"success": false,
    				"error":   "Either email or password is incorrect",
    			})
    			return
    		}
    		log.Println("failed to search for user")
    	}
    
    	err = bcrypt.CompareHashAndPassword([]byte(dbUser.Password), []byte(user.Password))
    	if err != nil {
    		log.Println("password is different")
    		ctx.JSON(http.StatusUnauthorized, gin.H{
    			"success": false,
    			"error":   "Either email or password is incorrect",
    		})
    		return
    	}
    
    	if !user.IsVerified {
    		ctx.JSON(http.StatusOK, gin.H{
    			"success":    false,
    			"isVerified": false,
    			"error":      "user not verified",
    		})
    	}
    
    	ctx.JSON(http.StatusOK, gin.H{
    		"success":    true,
    		"isVerified": false,
    		"error":      "",
    	})
    }
    ```
    
8. Let's create our handler function for `verifyCode` and `verifyLink`.
    
    We will read the link's email, hash and code from the request's query params. Compare the hash or code with the one retrieved from the database. If successful, we can then update the `isVerified` field in the Mongo database.
    
    Here we use the `coll.UpdateOne` to update our document based on the `_id` of the document.
    
    ```go
    func verifyCode(ctx *gin.Context) {
    	// read from query params
    	email := ctx.Query("email")
    	hash := ctx.Query("hash")
    	code := ctx.Query("code")
    
    	var user User
    
    	coll := client.Database("auth").Collection("users")
    	filter := bson.D{{"email", email}}
    	err := coll.FindOne(context.TODO(), filter).Decode(&user)
    	if err != nil {
    		if err == mongo.ErrNoDocuments {
    			log.Println(fmt.Sprintf("searching for users with email: %s yield no result found", email))
    		}
    		log.Println("failed to search for user")
    	}
    
    	// set user to verified
    	if user.VerifyCode == code && user.ValidCode.After(time.Now()) {
    		id, _ := primitive.ObjectIDFromHex(user.ID)
    		filter = bson.D{{"_id", id}}
    		update := bson.D{{"$set", bson.D{{"isVerified", true}}}}
    		result, err := coll.UpdateOne(context.TODO(), filter, update)
    		if err != nil {
    			log.Println("failed to update:", err)
    		}
    		log.Println(result)
    		ctx.JSON(http.StatusOK, gin.H{
    			"success": true,
    		})
    		return
    	}
    
    	ctx.JSON(http.StatusOK, gin.H{
    		"success": false,
    		"error":   "incorrect code",
    	})
    }
    ```
    
9. Let's implement the helper functions we used earlier.
    
    `generateVerifyCode` will randomly generate an Int between 0 to 9999 and then pad the value such that it is 4 characters long
    
    `generateVerifyHash` will randomly generate random bytes in our array and then change this value by encoding it into a string via base64 encoding.
    
    ```go
    func generateVerifyCode() string {
    	rand.Seed(time.Now().UnixNano())
    	randomNumber := rand.Intn(10000)
    	paddedNumber := fmt.Sprintf("%04d", randomNumber)
    
    	return paddedNumber
    }
    
    func generateVerifyHash() string {
    	charLen := 50
    	randomBytes := make([]byte, charLen)
    
    	_, err := rand.Read(randomBytes)
    	if err != nil {
    		fmt.Println("failed to generate random bytes")
    	}
    
    	randomString := base64.URLEncoding.EncodeToString(randomBytes)
    	return randomString[:charLen]
    }
    ```
    
10. Set up our connection with the SMTP host. We then use the `net/smtp` package from Go to construct and send our email.
    
    ```go
    const (
    	// configs to connect to gmail smtp server
    	smtpHost = "smtp.gmail.com"
    	smtpPort = 587
    )
    
    appkey = os.Getenv("APPKEY")
    hostEmail = os.Getenv("HOSTEMAIL")
    
    func getVerifyLink(email string, verifyHash string, verifyCode string) string {
    	return fmt.Sprintf("http://localhost:3001/verify/link?email=%s&hash=%s&code=%s", email, verifyHash, verifyCode)
    }
    
    func sendVerificationEmail(user User) {
    	verifyLink := getVerifyLink(user.Email, user.VerifyHash, user.VerifyCode)
    	to := []string{user.Email}
    	subject := "Email verification"
    	body := fmt.Sprintf("Hello, \r\n\r\nThis email contains the verification link and verification code, please the link below to verify your account.\r\n\r\nVerification link: %s\r\n\r\nAlternatively, u can also key in the verification code to verify.\r\n\r\nVerification code: %s", verifyLink, user.VerifyCode)
    
    	message := []byte("To: " + user.Email + "\r\n" +
    		"From: " + hostEmail + "\r\n" +
    		"Subject: " + subject + "\r\n" +
    		"\r\n" +
    		body + "\r\n")
    
    	auth := smtp.PlainAuth("", hostEmail, appkey, smtpHost)
    
    	err := smtp.SendMail("smtp.gmail.com:587", auth, hostEmail, to, message)
    	if err != nil {
    		log.Println("Failed to send email")
    		log.Println(err)
    		return
    	}
    	log.Println("Email sent successfully")
    }
    ```
    

## Setting up frontend

1. Initialize a new Next.js project using the command below and name the folder **frontend**
    
    ```bash
    npx create-next-app@latest
    ```
    
2. In the root frontend, create a folder **components** that holds our styled-components for Fields, Buttons and ButtonText.
    
    ```typescript
    // component.tsx
    
    export const Field = ({ title, placeholder = '', type = 'text', value, onChange }:
        { title: string, placeholder?: string, type?: string, value: string, onChange: Function }) => {
        return (
            <div>
                <h3>{title}</h3>
                <input className="py-2 px-4 leading-tight text-gray-700 appearance-none border-4 border-gray-200 rounded focus:outline-none focus:bg-white focus:border-teal-500"
                    placeholder={placeholder} type={type} value={value} onChange={(e) => onChange(e.target.value)} />
            </div>
        )
    }
    
    export const Button = ({ text, onClick }: { text: string, onClick?: Function }) => {
        return (
           <button onClick={() => { onClick?.() }} 
                className="px-7 py-3 mt-10 bg-teal-500 hover:bg-teal-700 border rounded-lg font-bold">{text}</button>
        )
    }
    
    export const ButtonText = ({ text, onClick }: { text: string, onClick: Function }) => {
        return (
            <button onClick={() => { onClick() }} 
                className="px-7 py-3 mt-10 hover:bg-gray-600 rounded-lg font-bold">{text}</button>
        )
    }
    ```
    
3. Under **/app/page.tsx**, replace the boiler code with our login, register and Verify components. They provide the fields for inputting our user's login or register info and calling the backend API on submit.
    
    ```typescript
    'use client'
    
    import { useState } from "react"
    import { useRouter } from "next/navigation"
    import { Button, ButtonText, Field } from "@/components/component"
    
    export default function Home() {
        const [isLogin, setLogin] = useState(true)
        const [showVerify, setVerify] = useState(false)
        const [email, setEmail] = useState("")
    
        return (
            <main className="min-h-screen flex flex-col items-center justify-center">
                <h1 className="text-4xl font-bold mb-10">Authentication demo page</h1>
                {
                    showVerify ? <Verify email={email}/> : isLogin 
                        ? <Login setLogin={setLogin} setVerify={setVerify}/>
                        : <Register setLogin={setLogin} setVerify={setVerify} email={email} setEmail={setEmail}/>
                }
            </main>
        )
    }
    
    function Register({ setLogin, setVerify, email, setEmail }:
        { setLogin: Function, setVerify: Function, email: string, setEmail: Function }) {
        const [name, setName] = useState("")
        const [password, setPassword] = useState("")
        const [repassword, setRepassword] = useState("")
        const [error, setError] = useState("")
    
        const register = () => {
            if (password !== repassword) {
                return
            }
            const user = {
                name,
                email,
                password
            }
            fetch("http://localhost:3001/register", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(user)
            })
                .then((resp) => resp.json())
                .then(({ success, error }) => {
                    if (success) {
                        setVerify(true);
                    } else {
                        // handle incorrect login 
                        setError(error)
                    }
                })
        }
    
        return (
            <>
                <h1 className="text-4xl font-bold mb-10">Register</h1>
                <div className="flex flex-col gap-5">
                    <Field title={'Name'} value={name} onChange={setName} />
                    <Field value={email} onChange={setEmail}
                        title={'Email'} placeholder="example@mail.com" type="email" />
                    <Field value={password} onChange={setPassword}
                        title={'Password'} type="password" />
                    <Field value={repassword} onChange={setRepassword}
                        title={'Retype Password'} type="password" />
                </div>
                <Button text={"Register"} onClick={register} />
                <ButtonText text={"Login instead"} onClick={() => setLogin(true)} />
            </>
        )
    }
    
    function Login({ setLogin, setVerify }: { setLogin: Function , setVerify: Function }) {
        const router = useRouter()
    
        const [email, setEmail] = useState("")
        const [password, setPassword] = useState("")
        const [error, setError] = useState("")
    
        const login = () => {
            const user = {
                email,
                password
            }
    
            fetch("http://localhost:3001/login", {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
                body: JSON.stringify(user)
            })
                .then((resp) => resp.json())
                .then(({ success, isVerified, error }) => {
                    if (success) {
                        if (isVerified) {
                            router.push("/dashboard")
                        }
                        setVerify(true);
                    } else {
                        // handle incorrect login 
                        setError(error)
                    }
                })
        }
    
        return (
            <>
                <h1 className="text-4xl font-bold mb-10">Login</h1>
                <div className="flex flex-col gap-5">
                    <Field value={email} onChange={setEmail}
                        title={'Email'} placeholder="example@mail.com" type="email" />
                    <Field value={password} onChange={setPassword}
                        title={'Password'} type="password" />
                </div>
                {error === "" ? <></> : <p className="my-2 text-red-800">{error}</p>}
                <Button text={"Login"} onClick={() => { login() }} />
                <ButtonText text={"Register instead"} onClick={() => setLogin(false)} />
            </>
        )
    }
    
    function Verify({ email }: {email: string}) {
    
        const router = useRouter()
        const [code, setCode] = useState("")
        const [error, setError] = useState("")
    
        const verify = () => {
            fetch(`http://localhost:3001/verify?email=${email}&code=${code}`, {
                method: "POST",
                headers: {
                    "Content-Type": "application/json",
                },
            })
                .then((resp) => resp.json())
                .then(({ success, error }) => {
                    if (success) {
                        router.push("/dashboard")
                    } else {
                        // handle incorrect login 
                        setError(error)
                    }
                })
        }
    
        return (
            <div className="flex flex-col gap-5 items-center justify-center">
                <h1 className="uppercase text-xl" >Verify your email address</h1>
                <div className="w-1/2 text-center">
                <p className="">A verification code and link has been sent to *****@****mail.com</p>
                </div>
                <p>Please check your inbox or junk folder and proceed to verify with us. The code will expire in 2mins</p>
                <Field title="Code" value={code} onChange={setCode} />
                <Button text={"Verify"} onClick={() => {verify()}} />
            </div>
        )
    }
    ```
    
4. Lastly, we create the dashboard route when the user successfully logins and also the verify route when the user clicks on the verify link which will prompt them to return to the homepage to log in again.
    
    ```typescript
    // in /app/dashboard/page.tsx
    "use client"
    export default function Page() {
        return (
            <div className="min-h-screen flex flex-col items-center justify-center">
                <h1 className="text-3xl mb-3">This is a dashboard viewable when logged in</h1>
                <h2>Return to root page using button below</h2>
                <Link href="/">
                    <Button text="Return"/>
                </Link>
            </div>
        )
    }
    ```
    
    ```typescript
    // in /app/verify/page.tsx
    "use client"
    export default function Page() {
        return (
            <div className="min-h-screen flex flex-col items-center justify-center">
                <h1 className="text-3xl mb-3">You are now verified!</h1>
                <h2>Click below to return back to the homepage and login</h2>
                <Link href="/">
                    <Button text="Return"/>
                </Link>
            </div>
        )
    }
    ```
    

## Testing our app

To start our Go server, run

```bash
go run .
```

> ensure that you have the `.env` file in the root of the backend folder. Be sure not to commit this into the git repository as the keys are private and should be kept hidden.
> 
> ```bash
> // .env file
> MONGODB_URI="mongodb+srv://<USER>:<PASSWORD>@cluster23.honxmor.mongodb.net/?retryWrites=true&w=majority"
> HOSTEMAIL="XXXXX@gmail.com"
> APPKEY="ABCDEFGH"
> ```

To start our Next.js frontend, run:

```bash
yarn dev
```

Our backend will be running in `localhost:3001` while our frontend will be running in `localhost:3000`.

If we try to register:

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684307369470/78d34fdb-190f-4f68-8c53-b991e433efdf.png align="center")

We will have a user entry created in MongoDB Atlas

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684307429435/4ce305ad-538c-4e9e-89a0-00309e604098.png align="center")

and also received my verification email

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684307479174/5eb6e272-e458-4b97-8363-7d6aa796560e.png align="center")

which we can proceed to click the link or fill up the verification code on our page

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1684307536382/b2aaa2b8-a27c-48a3-a266-ce27bdf4f8c4.png align="center")

## Summary

We have created an application that can send verification emails for users to verify.

In the future, I will try to optimize our backend code so that the frontend side can receive the response from the server faster.
