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.
Before diving into the implementation, make sure to have the following prerequisites:
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.
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.
Make backend folder
mkdir backend
cd backend
Set up a new Go module and install necessary dependencies
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
Set up the API server using gin and include cors settings so that we do not run into any "Blocked by CORS policy" errors.
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")
}
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.
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
}
}
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.
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"`
}
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.
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
}
hash, err := bcrypt.GenerateFromPassword([]byte(newUser.Password), 6)
if err != nil {
log.Println("error hashing password")
}
newUser.Password = string(hash)
newUser.VerifyHash = generateVerifyHash()
newUser.VerifyCode = generateVerifyCode()
now := time.Now()
newUser.DateCreated = now
newUser.ValidHash = now.Add(15 * time.Minute)
newUser.ValidCode = now.Add(3 * time.Minute)
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)
sendVerificationEmail(newUser)
ctx.JSON(http.StatusOK, gin.H{
"success": true,
})
}
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.
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": "",
})
}
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.
func verifyCode(ctx *gin.Context) {
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")
}
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",
})
}
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.
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]
}
Set up our connection with the SMTP host. We then use the net/smtp package from Go to construct and send our email.
const (
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")
}
Initialize a new Next.js project using the command below and name the folder frontend
npx create-next-app@latest
In the root frontend, create a folder components that holds our styled-components for Fields, Buttons and ButtonText.
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>
)
}
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.
'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 {
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 {
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 {
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>
)
}
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.
"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>
)
}
"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>
)
}
which we can proceed to click the link or fill up the verification code on our page
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.