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?
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:
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
|
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
SpanishAdding 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 buffaloIn 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"}
|
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! ❤️