In this article, you’ll learn how to implement forget/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 a POST request 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 a PATCH 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 the models/user.model.go
file. We’ll use these structs to validate the request body when implementing the forget/reset password functionality.
models/user.model.go
// ? ForgotPasswordInput struct
type ForgotPasswordInput struct {
Email string `json:"email" binding:"required"`
}
// ? ResetPasswordInput struct
type ResetPasswordInput struct {
Password string `json:"password" binding:"required"`
PasswordConfirm string `json:"passwordConfirm" binding:"required"`
}
The models/user.model.go
file should now look like this:
models/user.model.go
// ? SignUpInput struct
type 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 struct
type SignInInput struct {
Email string `json:"email" bson:"email" binding:"required"`
Password string `json:"password" bson:"password" binding:"required"`
}
// ? DBResponse struct
type 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 struct
type 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 struct
type ForgotPasswordInput struct {
Email string `json:"email" binding:"required"`
}
// ? ResetPasswordInput struct
type 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 official html/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 standard html/template
package instead.
These Email templates are based on a free HTML email template I cloned from GitHub.
Now create a templates folder in the root directory of your project and create a styles.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 a base.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 the styles.html
file in the base.html
file, I used the Golang template action which is similar to the Jinja2 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 a resetPassword.html
child template to extend the base.html
template and override the content 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 a ParseTemplateDir()
function to load and parse all the HTML files in the templates folder.
The ParseTemplateDir()
function accepts the path to the HTML files and returns the parsed HTML templates.
utils/email.go
// ? Email template parser
func ParseTemplateDir(dir string) (*template.Template, error) {
var paths []string
err := 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 the Gomail package that will be used to send the SMTP emails.
go get gopkg.in/gomail.v2
Also, install the HTML2Text package to convert the HTML template to text.
go get github.com/k3a/html2text
Now 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 string
FirstName string
Subject string
}
// ? Email template parser
func 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.EmailFrom
smtpPass := config.SMTPPass
smtpUser := config.SMTPUser
to := user.Email
smtpHost := config.SMTPHost
smtpPort := config.SMTPPort
var body bytes.Buffer
template, 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 Email
if err := d.DialAndSend(m); err != nil {
return err
}
return nil
}
Add the Forgot Password Controller
Create a ForgotPassword()
controller in the controllers
folder.
controllers/auth.controller.go
func (ac *AuthController) ForgotPassword(ctx *gin.Context) {
var userCredential *models.ForgotPasswordInput
if 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 Code
resetToken := randstr.String(20)
passwordResetToken := utils.Encode(resetToken)
// Update User in Database
query := 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.Name
if strings.Contains(firstName, " ") {
firstName = strings.Split(firstName, " ")[1]
}
// ? Send Email
emailData := 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
ForgotPasswordInput
struct we defined in theuser.model.go
to 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.
Read API 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 the ResetPassword
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.ResetPasswordInput
if 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 Database
query := 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 request parameters and hashed it.
- Next, I passed the
ResetPasswordInput
struct 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.
Read Golang & MongoDB: JWT Authentication and Authorization to know about theHashPassword()
function. - Also, I used the
$unset
operator to delete thepasswordResetToken
andpasswordResetAt
fields from the database. - Finally, I sent expired cookies to the user’s browser.
Update the Routes
Next, add the following routes to the AuthRoute()
method.
To request a password reset token, the user will make a POST request to /api/auth/forgotpassword
with his email.
After receiving the token, the user will then make a PATCH 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)
The AuthRoute()
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: