# Buffalo API with Authentication Mailer
In my previous post [Buffalo API with JWT Authentication]({{< ref "buffalo-auth.md" >}}), 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?*
* [Buffalo](https://gobuffalo.io/) with [Pop](https://github.com/gobuffalo/pop) like ORM
* [Golang](https://golang.org/)
* [PostgresSQL](https://www.postgresql.org/) & [pgAdmin](https://www.pgadmin.org/)
* [JWT](https://jwt.io/)
* [Gmail Account](https://mail.google.com/)
{{< admonition type=tip >}}
I recommend that you see the [previous post]({{< ref "buffalo-auth.md" >}}) or, you can [download the project](https://gitlab.com/herla97/auth-api/-/tree/v0.0.1) in my repository to follow this guide.
{{< /admonition >}}
### 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:
```zsh
$ buffalo pop g m validation
```
In our validation model, we'll add the following fields:
```go
// 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.
```fizz
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:
```zsh
$ buffalo db migrate up
```
{{< admonition type=warning >}}
If you haven't previously created the database, it is necessary to use the following command:
```zsh
$ buffalo db create -a
```
Show more [previous post]({{< ref "buffalo-auth/#database-migration" >}})
{{< /admonition >}}
### Creating Mailers
To create our first mailer template, we will use the following command provided by [Buffalo Mails](https://gobuffalo.io/en/docs/mail)
```zsh
$ buffalo g mailer send_code
```
![New Mailers Dir's](/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
{{< admonition type=warning >}}
You need to configure the **Sign in using App Passwords** for your Gmail account.
See more [English](https://support.google.com/accounts/answer/185833?hl=en) or [Spanish](https://support.google.com/accounts/answer/185833?hl=es)
{{< /admonition >}}
#### Adding Environment Variables
First of all, it is necessary to add our environment variables in our **.env** file.
```env
# Mailer Config
HOST="smtp.gmail.com"
MAIL_PORT="587"
MAIL_USERNAME="youremail@gmail.com"
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:
```go
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:
```go
// 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.
{{< admonition type=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](https://gobuffalo.io/en/docs/templating/)
{{< /admonition >}}
In the `send_code.plush.html` file we add the following:
```html
Verification Code
Code: <%= code %>
```
### User Actions
We need to create a new function to send the verification code of a user. We add a new action as follows:
```zsh
$ buffalo g a users ForgotPassword -m "POST" --skip-template
```
In that function, we'll add the following code:
```go
// 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:
```go
// Disable Auth Middleware in these fuctions
app.Middleware.Skip(AuthMiddleware,
AuthLogin,
UsersCreate,
UsersForgotPassword)
app.POST("/users/forgot_password", UsersForgotPassword)
```
**Let's try**
```zsh
curl -XPOST -H "Content-type: application/json" -d '{
"email": "youremail@test.com"
}' 'http://127.0.0.1:3000/users/forgot_password'
```
**Output**
```zsh
{"message":"We send a verification code to your email","user_id":"d7cef773-2502-4878-b79f-877b13744e16"}
```
![It's Works](/images/buffalo-mailers/ss1.png)
### Validations Actions
Once you send the verification codes, it is necessary to validate them.
We create the following function:
```zsh
$ buffalo g a validations ForgotPasswordCode -m "POST" --skip-template
```
Buffalo automatically generates our `validations.go` file, so we add the following to our function:
```go
// 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:
```go
// Disable Auth Middleware in these fuctions
app.Middleware.Skip(AuthMiddleware,
AuthLogin,
UsersCreate,
UsersForgotPassword,
ValidationsForgotPasswordCode)
app.POST("/validations/{user_id}", ValidationsForgotPasswordCode)
```
**Testing**
```zsh
curl -XPOST -H "Content-type: application/json" -d '{
"code": "1774"
}' 'http://127.0.0.1:3000/validations/d7cef773-2502-4878-b79f-877b13744e16'
```
**Output**
```json
{
"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.
```zsh
$ buffalo g a users UpdatePassword -m "PATCH" --skip-template
```
We add the following function:
```go
// 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**
```zsh
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**
```json
{
"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](https://gitlab.com/herla97/auth-api/-/tree/v0.0.3).
{{< admonition type=tip >}}
If you want to use this in production I recommend using [Sendgrid](https://sendgrid.com/). 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.
{{< /admonition >}}
> **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](https://paypal.me/shernandezlara)
Thanks! ❤️