API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
In this article, you’ll learn how to implementforget/reset password functionality with Golang, Gin Gonic, Gomail, MongoDB-Go-driver, Redis, and Docker-compose.
CRUD RESTful API with Golang + MongoDB Series:
- API with Golang + MongoDB + Redis + Gin Gonic: Project Setup
- Golang & MongoDB: JWT Authentication and Authorization
- API with Golang + MongoDB: Send HTML Emails with Gomail
- API with Golang, Gin Gonic & MongoDB: Forget/Reset Password
- Build Golang gRPC Server and Client: SignUp User & Verify Email
- Build Golang gRPC Server and Client: Access & Refresh Tokens
- Build CRUD RESTful API Server with Golang, Gin, and MongoDB
Related Article:

Forget/Reset Password with Golang, Gin, and MongoDB
| HTTP METHOD | ROUTE | DESCRIPTION |
|---|---|---|
| POST | /api/auth/forgotpassword | To request a reset token |
| PATCH | /api/auth/resetpassword/:resetToken | To reset the password |
To request a password reset token, the user will make aPOSTrequest with his email address to the Golang API server.

The Golang server then validates the email, generates the password reset token, and sends the reset token to the user’s email.
Also, the server returns a message to the frontend application indicating that the password reset email has been successfully sent.

The user then opens the email and clicks on the“Reset Password” button.

Upon clicking the“Reset Password” button, the user is taken to the password reset page where he is required to enter his new password and confirm the password before making aPATCH request to the server.

The Golang server then validates the password reset token and updates the user’s password in the database before returning a success message to the frontend application.
The frontend application receives the success message and redirects the user to the login page.

Update the MongoDB Model Structs
Add the following structs to themodels/user.model.go file. We’ll use these structs to validate the request body when implementing theforget/reset password functionality.
models/user.model.go
// ? ForgotPasswordInput structtype ForgotPasswordInput struct {Email string `json:"email" binding:"required"`}// ? ResetPasswordInput structtype ResetPasswordInput struct {Password string `json:"password" binding:"required"`PasswordConfirm string `json:"passwordConfirm" binding:"required"`}Themodels/user.model.go file should now look like this:
models/user.model.go
// ? SignUpInput structtype SignUpInput struct {Name string `json:"name" bson:"name" binding:"required"`Email string `json:"email" bson:"email" binding:"required"`Password string `json:"password" bson:"password" binding:"required,min=8"`PasswordConfirm string `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`Role string `json:"role" bson:"role"`Verified bool `json:"verified" bson:"verified"`CreatedAt time.Time `json:"created_at" bson:"created_at"`UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`}// ? SignInInput structtype SignInInput struct {Email string `json:"email" bson:"email" binding:"required"`Password string `json:"password" bson:"password" binding:"required"`}// ? DBResponse structtype DBResponse struct {ID primitive.ObjectID `json:"id" bson:"_id"`Name string `json:"name" bson:"name"`Email string `json:"email" bson:"email"`Password string `json:"password" bson:"password"`PasswordConfirm string `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`Role string `json:"role" bson:"role"`Verified bool `json:"verified" bson:"verified"`CreatedAt time.Time `json:"created_at" bson:"created_at"`UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`}// ? UserResponse structtype UserResponse struct {ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`Name string `json:"name,omitempty" bson:"name,omitempty"`Email string `json:"email,omitempty" bson:"email,omitempty"`Role string `json:"role,omitempty" bson:"role,omitempty"`CreatedAt time.Time `json:"created_at" bson:"created_at"`UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`}// ? ForgotPasswordInput structtype ForgotPasswordInput struct {Email string `json:"email" binding:"required"`}// ? ResetPasswordInput structtype ResetPasswordInput struct {Password string `json:"password" binding:"required"`PasswordConfirm string `json:"passwordConfirm" binding:"required"`}func FilteredResponse(user *DBResponse) UserResponse {return UserResponse{ID: user.ID,Email: user.Email,Name: user.Name,Role: user.Role,CreatedAt: user.CreatedAt,UpdatedAt: user.UpdatedAt,}}Create the HTML Email Templates with Golang
To send the HTML Emails, we’ll use the officialhtml/template package that comes with Golang to generate the HTML Email templates.
There are other template engines like jigo, pongo2, or mustache, that we could have used but I decided to use the standardhtml/template package instead.
These Email templates are based on afree HTML email template I cloned from GitHub.
Now create a templates folder in the root directory of your project and create astyles.html file.
templates/styles.html
{{define "styles"}}<style> /* ------------------------------------- GLOBAL RESETS ------------------------------------- */ /*All the styling goes here*/ img { border: none; -ms-interpolation-mode: bicubic; max-width: 100%; } body { background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; } table { border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; } table td { font-family: sans-serif; font-size: 14px; vertical-align: top; } /* ------------------------------------- BODY & CONTAINER ------------------------------------- */ .body { background-color: #f6f6f6; width: 100%; } /* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */ .container { display: block; margin: 0 auto !important; /* makes it centered */ max-width: 580px; padding: 10px; width: 580px; } /* This should also be a block element, so that it will fill 100% of the .container */ .content { box-sizing: border-box; display: block; margin: 0 auto; max-width: 580px; padding: 10px; } /* ------------------------------------- HEADER, FOOTER, MAIN ------------------------------------- */ .main { background: #ffffff; border-radius: 3px; width: 100%; } .wrapper { box-sizing: border-box; padding: 20px; } .content-block { padding-bottom: 10px; padding-top: 10px; } .footer { clear: both; margin-top: 10px; text-align: center; width: 100%; } .footer td, .footer p, .footer span, .footer a { color: #999999; font-size: 12px; text-align: center; } /* ------------------------------------- TYPOGRAPHY ------------------------------------- */ h1, h2, h3, h4 { color: #000000; font-family: sans-serif; font-weight: 400; line-height: 1.4; margin: 0; margin-bottom: 30px; } h1 { font-size: 35px; font-weight: 300; text-align: center; text-transform: capitalize; } p, ul, ol { font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; margin-bottom: 15px; } p li, ul li, ol li { list-style-position: inside; margin-left: 5px; } a { color: #3498db; text-decoration: underline; } /* ------------------------------------- BUTTONS ------------------------------------- */ .btn { box-sizing: border-box; width: 100%; } .btn > tbody > tr > td { padding-bottom: 15px; } .btn table { width: auto; } .btn table td { background-color: #ffffff; border-radius: 5px; text-align: center; } .btn a { background-color: #ffffff; border: solid 1px #3498db; border-radius: 5px; box-sizing: border-box; color: #3498db; cursor: pointer; display: inline-block; font-size: 14px; font-weight: bold; margin: 0; padding: 12px 25px; text-decoration: none; text-transform: capitalize; } .btn-primary table td { background-color: #3498db; } .btn-primary a { background-color: #3498db; border-color: #3498db; color: #ffffff; } /* ------------------------------------- OTHER STYLES THAT MIGHT BE USEFUL ------------------------------------- */ .last { margin-bottom: 0; } .first { margin-top: 0; } .align-center { text-align: center; } .align-right { text-align: right; } .align-left { text-align: left; } .clear { clear: both; } .mt0 { margin-top: 0; } .mb0 { margin-bottom: 0; } .preheader { color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0; } .powered-by a { text-decoration: none; } hr { border: 0; border-bottom: 1px solid #f6f6f6; margin: 20px 0; } /* ------------------------------------- RESPONSIVE AND MOBILE FRIENDLY STYLES ------------------------------------- */ @media only screen and (max-width: 620px) { table.body h1 { font-size: 28px !important; margin-bottom: 10px !important; } table.body p, table.body ul, table.body ol, table.body td, table.body span, table.body a { font-size: 16px !important; } table.body .wrapper, table.body .article { padding: 10px !important; } table.body .content { padding: 0 !important; } table.body .container { padding: 0 !important; width: 100% !important; } table.body .main { border-left-width: 0 !important; border-radius: 0 !important; border-right-width: 0 !important; } table.body .btn table { width: 100% !important; } table.body .btn a { width: 100% !important; } table.body .img-responsive { height: auto !important; max-width: 100% !important; width: auto !important; } } /* ------------------------------------- PRESERVE THESE STYLES IN THE HEAD ------------------------------------- */ @media all { .ExternalClass { width: 100%; } .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div { line-height: 100%; } .apple-link a { color: inherit !important; font-family: inherit !important; font-size: inherit !important; font-weight: inherit !important; line-height: inherit !important; text-decoration: none !important; } #MessageViewBody a { color: inherit; text-decoration: none; font-size: inherit; font-family: inherit; font-weight: inherit; line-height: inherit; } .btn-primary table td:hover { background-color: #34495e !important; } .btn-primary a:hover { background-color: #34495e !important; border-color: #34495e !important; } }</style>{{end}}Next, create abase.html file that will be extended by other HTML templates to generate different kinds of HTML templates like the Welcome Email template, Verification Email template, Password Reset Email template, and more.
To include thestyles.html file in thebase.html file, I used the Golangtemplate action which is similar to theJinja2 include tag.
Lastly, I created a block named content which will be overridden by a child template.
templates/base.html
{{define "base"}}<!DOCTYPE html><html> <head> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> {{template "styles" .}} <title>{{ .Subject}}</title> </head> <body> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body" > <tr> <td> </td> <td class="container"> <div class="content"> <!-- START CENTERED WHITE CONTAINER --> {{block "content" .}}{{end}} <!-- END CENTERED WHITE CONTAINER --> </div> </td> <td> </td> </tr> </table> </body></html>{{end}}Now create aresetPassword.html child template to extend thebase.html template and override thecontent block in the base template.
templates/resetPassword.html
{{template "base" .}} {{define "content"}}<table role="presentation" class="main"> <!-- START MAIN CONTENT AREA --> <tr> <td class="wrapper"> <table role="presentation" border="0" cellpadding="0" cellspacing="0"> <tr> <td> <p>Hi {{ .FirstName}},</p> <p> Forgot password? Send a PATCH request to with your password and passwordConfirm to {{.URL}} </p> <table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" > <tbody> <tr> <td align="left"> <table role="presentation" border="0" cellpadding="0" cellspacing="0" > <tbody> <tr> <td> <a href="{{.URL}}" target="_blank" >Reset password</a > </td> </tr> </tbody> </table> </td> </tr> </tbody> </table> <p>If you didn't forget your password, please ignore this email</p> <p>Good luck! Codevo CEO.</p> </td> </tr> </table> </td> </tr> <!-- END MAIN CONTENT AREA --></table>{{end}}Define a Utility Function to Parse the HTML Templates
Next, create aParseTemplateDir() function to load and parse all the HTML files in the templates folder.
TheParseTemplateDir() function accepts the path to the HTML files and returns the parsed HTML templates.
utils/email.go
// ? Email template parserfunc ParseTemplateDir(dir string) (*template.Template, error) {var paths []stringerr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {if err != nil {return err}if !info.IsDir() {paths = append(paths, path)}return nil})fmt.Println("Am parsing templates...")if err != nil {return nil, err}return template.ParseFiles(paths...)}Create a Function to Send the HTML Emails
Run the following command to install theGomail package that will be used to send the SMTP emails.
go get gopkg.in/gomail.v2Also, install theHTML2Text package to convert the HTML template to text.
go get github.com/k3a/html2textNow let’s create a function to dynamically access and populate a specific parsed template before sending the HTML Email to the user.
utils/email.go
type EmailData struct {URL stringFirstName stringSubject string}// ? Email template parserfunc SendEmail(user *models.DBResponse, data *EmailData, templateName string) error {config, err := config.LoadConfig(".")if err != nil {log.Fatal("could not load config", err)}// Sender data.from := config.EmailFromsmtpPass := config.SMTPPasssmtpUser := config.SMTPUserto := user.EmailsmtpHost := config.SMTPHostsmtpPort := config.SMTPPortvar body bytes.Buffertemplate, err := ParseTemplateDir("templates")if err != nil {log.Fatal("Could not parse template", err)}template = template.Lookup(templateName)template.Execute(&body, &data)fmt.Println(template.Name())m := gomail.NewMessage()m.SetHeader("From", from)m.SetHeader("To", to)m.SetHeader("Subject", data.Subject)m.SetBody("text/html", body.String())m.AddAlternative("text/plain", html2text.HTML2Text(body.String()))d := gomail.NewDialer(smtpHost, smtpPort, smtpUser, smtpPass)d.TLSConfig = &tls.Config{InsecureSkipVerify: true}// Send Emailif err := d.DialAndSend(m); err != nil {return err}return nil}Add the Forgot Password Controller
Create aForgotPassword() controller in thecontrollers folder.
controllers/auth.controller.go
func (ac *AuthController) ForgotPassword(ctx *gin.Context) {var userCredential *models.ForgotPasswordInputif err := ctx.ShouldBindJSON(&userCredential); err != nil {ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})return}message := "You will receive a reset email if user with that email exist"user, err := ac.userService.FindUserByEmail(userCredential.Email)if err != nil {if err == mongo.ErrNoDocuments {ctx.JSON(http.StatusOK, gin.H{"status": "fail", "message": message})return}ctx.JSON(http.StatusBadGateway, gin.H{"status": "error", "message": err.Error()})return}if !user.Verified {ctx.JSON(http.StatusUnauthorized, gin.H{"status": "error", "message": "Account not verified"})return}config, err := config.LoadConfig(".")if err != nil {log.Fatal("Could not load config", err)}// Generate Verification CoderesetToken := randstr.String(20)passwordResetToken := utils.Encode(resetToken)// Update User in Databasequery := bson.D{{Key: "email", Value: strings.ToLower(userCredential.Email)}}update := bson.D{{Key: "$set", Value: bson.D{{Key: "passwordResetToken", Value: passwordResetToken}, {Key: "passwordResetAt", Value: time.Now().Add(time.Minute * 15)}}}}result, err := ac.collection.UpdateOne(ac.ctx, query, update)if result.MatchedCount == 0 {ctx.JSON(http.StatusBadGateway, gin.H{"status": "success", "message": "There was an error sending email"})return}if err != nil {ctx.JSON(http.StatusForbidden, gin.H{"status": "success", "message": err.Error()})return}var firstName = user.Nameif strings.Contains(firstName, " ") {firstName = strings.Split(firstName, " ")[1]}// ? Send EmailemailData := utils.EmailData{URL: config.Origin + "/resetpassword/" + resetToken,FirstName: firstName,Subject: "Your password reset token (valid for 10min)",}err = utils.SendEmail(user, &emailData, "resetPassword.html")if err != nil {ctx.JSON(http.StatusBadGateway, gin.H{"status": "success", "message": "There was an error sending email"})return}ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": message})}Here is a breakdown of what I did in the above snippets:
- First, I passed the
ForgotPasswordInputstruct we defined in theuser.model.goto theShouldBindJSON()function to help Gin return a validation error if the email was not provided in the request body. - Then I made a query to the MongoDB database to check if a user with that email exists before checking if the user is verified.
- Next, I generated the password reset token with the
Encode()and hashed it.
ReadAPI with Golang + MongoDB: Send HTML Emails with Gomail to know more about theEncode()function. - Finally, I sent the unhashed password reset token to the user’s email and stored the hashed one in the MongoDB database.
Add the Reset Password Controller
Now let’s create theResetPassword controller to validate the reset token and update the user’s password in the MongoDB database.
controllers/auth.controller.go
func (ac *AuthController) ResetPassword(ctx *gin.Context) { resetToken := ctx.Params.ByName("resetToken")var userCredential *models.ResetPasswordInputif err := ctx.ShouldBindJSON(&userCredential); err != nil {ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": err.Error()})return}if userCredential.Password != userCredential.PasswordConfirm {ctx.JSON(http.StatusBadRequest, gin.H{"status": "fail", "message": "Passwords do not match"})return}hashedPassword, _ := utils.HashPassword(userCredential.Password)passwordResetToken := utils.Encode(resetToken)// Update User in Databasequery := bson.D{{Key: "passwordResetToken", Value: passwordResetToken}}update := bson.D{{Key: "$set", Value: bson.D{{Key: "password", Value: hashedPassword}}}, {Key: "$unset", Value: bson.D{{Key: "passwordResetToken", Value: ""}, {Key: "passwordResetAt", Value: ""}}}}result, err := ac.collection.UpdateOne(ac.ctx, query, update)if result.MatchedCount == 0 {ctx.JSON(http.StatusBadRequest, gin.H{"status": "success", "message": "Token is invalid or has expired"})return}if err != nil {ctx.JSON(http.StatusForbidden, gin.H{"status": "success", "message": err.Error()})return}ctx.SetCookie("access_token", "", -1, "/", "localhost", false, true)ctx.SetCookie("refresh_token", "", -1, "/", "localhost", false, true)ctx.SetCookie("logged_in", "", -1, "/", "localhost", false, true)ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": "Password data updated successfully"})}Here is a summary of what I did in the above code snippets:
- First, I retrieved the password reset token from the requestparameters and hashed it.
- Next, I passed the
ResetPasswordInputstruct toShouldBindJSON()method to check if the user provided the required fields in the request body. - Then I validated the new password against the password confirm and returned an error if they are not equal.
- Next, I hashed the new password with the
HashPassword()function before calling theUpdateOne()MongoDB function to update the user’s password in the database.
ReadGolang & MongoDB: JWT Authentication and Authorization to know about theHashPassword()function. - Also, I used the
$unsetoperator to delete thepasswordResetTokenandpasswordResetAtfields from the database. - Finally, I sent expired cookies to the user’s browser.
Update the Routes
Next, add the following routes to theAuthRoute() method.
To request a password reset token, the user will make aPOST request to/api/auth/forgotpassword with his email.
After receiving the token, the user will then make aPATCH request with the new password and password confirm to/api/auth/resetpassword/:resetToken .
routes/auth.routes.go
router.POST("/forgotpassword", rc.authController.ForgotPassword)router.PATCH("/resetpassword/:resetToken", rc.authController.ResetPassword)TheAuthRoute() function should now look like this:
routes/auth.routes.go
type AuthRouteController struct {authController controllers.AuthController}func NewAuthRouteController(authController controllers.AuthController) AuthRouteController {return AuthRouteController{authController}}func (rc *AuthRouteController) AuthRoute(rg *gin.RouterGroup, userService services.UserService) {router := rg.Group("/auth")router.POST("/register", rc.authController.SignUpUser)router.POST("/login", rc.authController.SignInUser)router.GET("/refresh", rc.authController.RefreshAccessToken)router.GET("/logout", middleware.DeserializeUser(userService), rc.authController.LogoutUser)router.GET("/verifyemail/:verificationCode", rc.authController.VerifyEmail)router.POST("/forgotpassword", rc.authController.ForgotPassword)router.PATCH("/resetpassword/:resetToken", rc.authController.ResetPassword)}Conclusion
Congrats on reaching the end. In this article, you learned how to implement forget/reset password functionality with Golang, Gin Gonic, MongoDB, Redis, and Docker-compose.
Check out the source codes:
API with Node.js, Prisma & PostgreSQL: Forget/Reset Password
Build Golang gRPC Server and Client: SignUp User & Verify Email
One Comment
- kukuhon March 3, 2025Reply
Could not load environment variablesConfig File “app” Not Found
this my error message in terminal vscode in API with Golang, Gin Gonic & MongoDB: Forget/Reset Password articles can you help me for fix this error? thank you
Leave a ReplyCancel reply
This site uses Akismet to reduce spam.Learn how your comment data is processed.
Support Me!

Recent posts
Categories
- C#(2)
- C++(1)
- CSS / SCSS(3)
- Deno(8)
- Golang(31)
- JavaScript(5)
- NextJs(38)
- NodeJS(32)
- Programming(19)
- Python(19)
- React(38)
- Rust(35)
- Svelte(5)
- Vue(7)

