Contents

Buffalo API with Authentication Mailer

In my previous post Buffalo API with JWT Authentication, I talked about how to create an API using Buffalo and Golang

Now we’ll use the buffalo mailers to perform a password change through two-factor verification, using a code sent by email.

Before starting

What will we use?

Tip
I recommend that you see the previous post or, you can download the project in my repository to follow this guide.

I assume that …

  • You have installed Golang, Buffalo, and Postgres.

Let’s Go

Validations Model

We need to create a new model called validations, this will help us store generated codes to users who occupy them.

We’ll use the following command to generate our model:

1
$ buffalo pop g m validation

In our validation model, we’ll add the following fields:

1
2
3
4
5
6
7
8
9
// Validation is used by pop to map your validations database table to your go code.
type Validation struct {
	ID            uuid.UUID `json:"id" db:"id"`
	CreatedAt     time.Time `json:"created_at" db:"created_at"`
	UpdatedAt     time.Time `json:"updated_at" db:"updated_at"`
	ExpirationdAt time.Time `json:"expiration_at" db:"expiration_at"`
	User          uuid.UUID `json:"id_user" db:"id_user"`
	Code          string    `json:"code" db:"code"`
}

Now we will add those new fields to our up.fizz file to migrate the new table.

1
2
3
4
5
6
7
8
9
create_table("validations") {
	t.Column("id", "uuid", {primary: true})
	t.Timestamps()
	t.Column("expiration_at", "timestamp", {})
	t.Column("id_user", "uuid", {})
	t.Column("code", "string", {})

	t.ForeignKey("id_user", {"users": ["id"]}, {"on_delete": "cascade"})
}

Now we’ll migrate our new table:

1
$ buffalo db migrate up
Warning

If you haven’t previously created the database, it is necessary to use the following command:

1
$ buffalo db create -a  

Show more previous post

Creating Mailers

To create our first mailer template, we will use the following command provided by Buffalo Mails

1
$ buffalo g mailer send_code

/images/buffalo-mailers/vs1.png

Buffalo by default generates two folders, the mailers dir contains the controllers for sending emails and the templates dir contains our HTML files that will be sent by email.

Mailers Configuration

Warning
You need to configure the Sign in using App Passwords for your Gmail account. See more English or Spanish

Adding Environment Variables

First of all, it is necessary to add our environment variables in our .env file.

1
2
3
4
5
# Mailer Config
HOST="smtp.gmail.com"
MAIL_PORT="587"
MAIL_USERNAME="[email protected]"
MAIL_PASSWORD="yourAppPassword"

You should have obtained the password from the emial configuration.

Configuring Mailer Controllers

Now we configure our mailer controller by adding the environment variables. Also, add a function for generating random numbers bounded by a range of numbers.

In our dir of mailers, we’ll configure our file mailers.go as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package mailers

import (
	"log"
	"math/rand"
	"os"
	"time"

	"github.com/gobuffalo/buffalo/mail"
	"github.com/gobuffalo/buffalo/render"
	"github.com/gobuffalo/packr/v2"
)

var smtp mail.Sender
var r *render.Engine

func init() {
	var err error

	// Pulling config from the env.
	port := os.Getenv("MAIL_PORT")
	host := os.Getenv("HOST")
	user := os.Getenv("MAIL_USERNAME")
	password := os.Getenv("MAIL_PASSWORD")

	smtp, err = mail.NewSMTPSender(host, port, user, password)
	if err != nil {
		log.Fatal(err)
	}

	r = render.New(render.Options{
		TemplatesBox: packr.New("../templates/mail", "../templates/mail"),
	})
}

// createCode random number generation function for verification code.
func createCode(low, hi int) int {
	rand.Seed(time.Now().UnixNano())
	return low + rand.Intn(hi-low)
}

Once our init function of emails configured, we’ll create another function that will send the email. In our send_code.go file, we’ll add the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// SendCode Sending code by email
func SendCode(email string) (string, error) {
	var codeHashString string

	m := mail.NewMessage()

	// Create code fuction on range
	code := strconv.Itoa(createCode(1000, 9999))

	// fill in with your stuff:
	m.Subject = "Verification Code"
	m.From = os.Getenv("MAIL_USERNAME")
	m.To = []string{email}

	err := m.AddBody(r.HTML("send_code.html"), render.Data{"code": code})
	if err != nil {
		return codeHashString, err
	}

	err = smtp.Send(m)
	if err != nil {
		return codeHashString, err
	}

	// Generate email code hash.
	codehash, err := bcrypt.GenerateFromPassword([]byte(code), bcrypt.DefaultCost)
	if err != nil {
		return codeHashString, err
	}

	codeHashString = string(codehash)

	return codeHashString, nil
}

In this function, we generate a random code using our function sendCode and send that code to our template so that it can be displayed. Later we, encrypt the code and return it.

Mailers Templates

To finish our configuration, buffalo generates by default the following templates/mail dir, in which our send_code.plush.html file will be found.

Note
In this guide, I will not focus on the characteristics of the templates, but you can see more information in the documentation of buffalo

In the send_code.plush.html file we add the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="utf-8">
</head>
   <body>
      <h2>Verification Code<h2>
      <h3>
         Code: <%= code %>
      </h3>
   </body>
</html>

User Actions

We need to create a new function to send the verification code of a user. We add a new action as follows:

1
$ buffalo g a users ForgotPassword -m "POST" --skip-template

In that function, we’ll add the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// UsersForgotPassword default implementation.
func UsersForgotPassword(c buffalo.Context) error {
	// Allocate an empty User
	user := &models.User{}

	// Bind Framework to the html form elements
	if err := c.Bind(user); err != nil {
		return errors.WithStack(err)
	}

	// Save var of Request JSON Post.
	useremail := user.Email

	if useremail == "" {
		return c.Error(http.StatusBadRequest, errors.New("Username cannot be empty"))
	}

	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}

	q := tx.Select("id").Where("email = ?", useremail)

	if err := q.First(user); err != nil {
		return c.Error(http.StatusNotFound, errors.New("User Not Found"))
	}

	// Send verification code
	codeHashString, err := mailers.SendCode(useremail)
	if err != nil {
		return errors.WithStack(err)
	}

	// Add values to validation model.
	validation := &models.Validation{
		User:          user.ID,
		Code:          codeHashString,
		ExpirationdAt: time.Now().Add(5 * time.Minute),
	}

	// Insert user id, code hash into validations
	err = tx.Create(validation)
	if err != nil {
		return errors.WithStack(err)
	}

	return c.Render(http.StatusOK, r.Auto(c, map[string]string{
		"message": "We send a verification code to your email",
		"user_id": user.ID.String()}))
}

In the previous function, we perform a search if the user exists, later we call our function to send a verification code and save the code hash in our DB.

I add a 5-minute expiration for each code you can add the one you want.

We check the path in our app.go file and add our function to skip middleware:

1
2
3
4
5
6
7
// Disable Auth Middleware in these fuctions
app.Middleware.Skip(AuthMiddleware, 
	AuthLogin, 
	UsersCreate, 
	UsersForgotPassword)

app.POST("/users/forgot_password", UsersForgotPassword)

Let’s try

1
2
3
curl -XPOST -H "Content-type: application/json" -d '{
"email": "[email protected]"
}' 'http://127.0.0.1:3000/users/forgot_password'

Output

1
{"message":"We send a verification code to your email","user_id":"d7cef773-2502-4878-b79f-877b13744e16"}

/images/buffalo-mailers/ss1.png

Validations Actions

Once you send the verification codes, it is necessary to validate them.

We create the following function:

1
$ buffalo g a validations ForgotPasswordCode -m "POST" --skip-template

Buffalo automatically generates our validations.go file, so we add the following to our function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// ValidationsForgotPasswordCode default implementation.
func ValidationsForgotPasswordCode(c buffalo.Context) error {
	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no se encontro ninguna conexión"))
	}
	// Get the JWT Key Secret from .env file.
	secret := os.Getenv("JWT_SECRET")

	// User Model
	user := &models.User{}

	// Validation Model
	validation := &models.Validation{}

	// Bind Framework to the html form elements
	if err := c.Bind(validation); err != nil {
		return errors.WithStack(err)
	}

	// Save input raw code from user.
	code := validation.Code

	q := tx.Order("created_at desc").Where("expiration_at >= ? and id_user = ?", time.Now(), c.Param("user_id"))

	if err := q.First(validation); err != nil {
		return c.Error(http.StatusNotFound, errors.New("Code expired"))
	}

	// find auth user with the user_id and user not soft deleted.
	if err := tx.Select("id").Find(user, c.Param("user_id")); err != nil {
		return c.Error(http.StatusNotFound, err)
	}

	// confirm that the given password matches the hashed password from the db
	if err := bcrypt.CompareHashAndPassword([]byte(validation.Code), []byte(code)); err != nil {
		return c.Error(http.StatusBadRequest, errors.New("Código de Validadación Invalido"))
	}

	// Generate token with 6 hours expiration time.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"id":   user.ID,
		"type": "change_password",
		"exp":  time.Now().Add(time.Minute * 5).Unix(),
	})

	tokenString, err := token.SignedString([]byte(secret))
	if err != nil {
		return errors.WithStack(err)
	}

	return c.Render(http.StatusOK, r.Auto(c, map[string]string{
		"message": "Verified User",
		"user_id": user.ID.String(),
		"token":   tokenString}))
}

In that function, we verify the code and generate a token with an identifier to know if that token was generated through that function.

Before testing our function, we have to add it to skip middleware and name the path in our app.go file as follows:

1
2
3
4
5
6
7
8
// Disable Auth Middleware in these fuctions
app.Middleware.Skip(AuthMiddleware,
	AuthLogin,
	UsersCreate,
	UsersForgotPassword,
	ValidationsForgotPasswordCode)

app.POST("/validations/{user_id}", ValidationsForgotPasswordCode)

Testing

1
2
3
curl -XPOST -H "Content-type: application/json" -d '{
"code": "1774"
}' 'http://127.0.0.1:3000/validations/d7cef773-2502-4878-b79f-877b13744e16'

Output

1
2
3
4
5
{
	"message":"Verified User",
	"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODc5MDcyNzYsImlkIjoiZDdjZWY3NzMtMjUwMi00ODc4LWI3OWYtODc3YjEzNzQ0ZTE2IiwidHlwZSI6InBhc3N3b3JkIn0.iVeTyS0qxGJa82r_BaATKDwY1z5c_siyhpNOevwo6eA",
	"user_id":"d7cef773-2502-4878-b79f-877b13744e16"
}

Update Password

Finally, we need to generate the last function to update the password, we will generate a new action in users.

1
$ buffalo g a users UpdatePassword -m "PATCH" --skip-template

We add the following function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// UsersUpdatePassword default implementation.
func UsersUpdatePassword(c buffalo.Context) error {
	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}

	// Allocate an empty User
	user := &models.User{}

	claims := c.Value("claims").(jwt.MapClaims)

	t := claims["type"].(string)
	if t != "password" {
		return c.Error(http.StatusUnauthorized, errors.New("Invalid Token"))
	}

	// Bind Framework to the html form elements
	if err := c.Bind(user); err != nil {
		return errors.WithStack(err)
	}

	// Validation password size
	if len(user.Password) < 6 || len(user.Password) > 50 {
		return c.Error(http.StatusNotAcceptable, errors.New("The password must be greater than 6 characters"))
	}

	// find auth user with the user_id and user not soft deleted.
	if err := tx.Select("id").Find(user, c.Param("user_id")); err != nil {
		return c.Error(http.StatusNotFound, err)
	}

	hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
	if err != nil {
		return errors.WithStack(err)
	}

	ph := string(hash)

	// RawQuery for update password user.
	err = tx.RawQuery("UPDATE users SET password = ? WHERE id = ?", ph, user.ID).Exec()
	if err != nil {
		return errors.WithStack(err)
	}

	return c.Render(http.StatusCreated, r.Auto(c, map[string]string{"message": "Updated Password"}))
}

In the previous function we read the claims extracted from the token and verify that the type of token is to change the password, we generate a new hash to the new password and this is saved in the DB.

Let’s try

1
2
3
curl -XPATCH -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODc5MDcyNzYsImlkIjoiZDdjZWY3NzMtMjUwMi00ODc4LWI3OWYtODc3YjEzNzQ0ZTE2IiwidHlwZSI6InBhc3N3b3JkIn0.iVeTyS0qxGJa82r_BaATKDwY1z5c_siyhpNOevwo6eA' -H "Content-type: application/json" -d '{
"password": "testing"
}' 'http://127.0.0.1:3000/users/update_password/d7cef773-2502-4878-b79f-877b13744e16'

Output

1
2
3
{
	"message":"Updated Password"
}

Congratulations, the password has been changed correctly. Now it’s your turn to try your new password.

You can see the project in my repository in Gitlab.

Tip
If you want to use this in production I recommend using Sendgrid. It is necessary to make some changes but it isn’t very complicated if you have any questions you can leave it in the comments.

If you liked the guide, share it and let me know, if you find any mistakes you can tell me we are here to learn from our mistakes and good luck. ¡Stay at Home!

You can also invite me for a coffee ☕️ Paypal

Thanks! ❤️