Introduction
Though we have provided a way for our users to register an account, our application won't compile yet. Some methods used ain't currently available and we haven't implemented the method that ACTUALLY sends the emails. We will fix that in this article. Also, we will provide a way for our users to activate their accounts.
Source code
The source code for this series is hosted on GitHub via:
go-auth
This repository accompanies a series of tutorials on session-based authentication using Go at the backend and JavaScript (SvelteKit) on the front-end.
It is currently live here (the backend may be brought down soon).
To run locally, kindly follow the instructions in each subdirectory.
Implementation
Step 1: Sending users emails
In the previous article, we made it important for users to verify the emails provided during registration by sending a token and activation instructions to such email addresses. We have not implemented the real email-sending logic yet. So, in internal/mailer/mailer.go
, we should have this:
// internal/mailer/mailer.go
package mailer
import (
"bytes"
"embed"
"html/template"
"time"
"gopkg.in/gomail.v2"
)
//go:embed "templates"
var templateFS embed.FS
type Mailer struct {
dialer *gomail.Dialer
sender string
}
func New(host string, port int, username, password, sender string) Mailer {
dialer := gomail.NewDialer(host, port, username, password)
return Mailer{
dialer: dialer,
sender: sender,
}
}
func (m Mailer) Send(recipient, templateFile string, data interface{}) error {
tmpl, err := template.New("email").ParseFS(templateFS, "templates/"+templateFile)
if err != nil {
return err
}
subject := new(bytes.Buffer)
err = tmpl.ExecuteTemplate(subject, "subject", data)
if err != nil {
return err
}
plainBody := new(bytes.Buffer)
err = tmpl.ExecuteTemplate(plainBody, "plainBody", data)
if err != nil {
return err
}
htmlBody := new(bytes.Buffer)
err = tmpl.ExecuteTemplate(htmlBody, "htmlBody", data)
if err != nil {
return err
}
msg := gomail.NewMessage()
msg.SetHeader("To", recipient)
msg.SetHeader("From", m.sender)
msg.SetHeader("Subject", subject.String())
msg.SetBody("text/plain", plainBody.String())
msg.AddAlternative("text/html", htmlBody.String())
for i := 1; i <= 3; i++ {
err = m.dialer.DialAndSend(msg)
if nil == err {
return nil
}
// If it didn't work, sleep for a short time and retry.
time.Sleep(500 * time.Millisecond)
}
return nil
}
From the imports, we need a third-party package to send the mail and gomail is pretty awesome for it. Next, we want Go to efficiently load our templates
folder so that we can just provide a template name and it will correctly load it without us writing a lot of code. Go's embed (available since v1.16) makes this easily possible. All you need is a variable, in our case templateFS
, with //go:embed
directive followed by the folder's name, at the top of the variable. Hence we had:
...
//go:embed "templates"
var templateFS embed.FS
This made Go efficiently "embed" the templates folder and appropriately load any file we provide.
Next is the Mailer
type. It is what will be fed into our application. From the previous registerUserHandler
, we saw something like this:
...
err = app.mailer.Send(user.Email, "user_welcome.tmpl", data)
...
The mailer
there is an instance of the Mailer
type and Send
is the method that really sends the email. This method requires us to provide the recipient
, templateFile
— in this case user_welcome.tmpl
— and data
. At the beginning of the Send
method, we connected the supplied template filename with the Go embed variable and then tries to execute our template file with the data
as its context. We provide both a plain text (marked by plainBody
in the template file) and HTML (marked by htmlBody
) based template in the same template file. Then, we construct the email and eventually send it out. user_welcome.tmpl
looks like this:
<!-- internal/mailer/templates/user_welcome.tmpl -->
{{define "subject"}}Welcome to John - GoAuth!{{end}}
{{define "plainBody"}}
Hello,
Thanks for signing up for a John - GoAuth account. We're excited to have you on board!
For future reference, your user ID number is {{.userID}}.
Please visit {{.frontendURL}}/auth/activate/{{.userID}} and input the token below to activate your account:
{{.token}}
Please note that this is a one-time use token and it will expire in {{.expiration}} ({{.exact}}).
Thanks,
The John - GoAuth Team
{{end}}
{{define "htmlBody"}}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<table style="background: #ffffff; border-radius: 1rem; padding: 30px 0px">
<tbody>
<tr>
<td style="padding: 0px 30px">
<h3 style="margin-bottom: 0px; color: #000000">Hello,</h3>
<p>
Thanks for signing up for a John - GoAuth account. We're excited
to have you on board!
</p>
</td>
</tr>
<tr>
<td style="padding: 0px 30px">
<p>For future reference, your user ID number is {{.userID}}.</p>
<p>
Please visit
<a href="{{.frontendURL}}/auth/activate/{{.userID}}">
{{.frontendURL}}/auth/activate/{{.userID}}
</a>
and input the OTP below to activate your account:
</p>
</td>
</tr>
<tr>
<td style="padding: 10px 30px; text-align: center">
<strong style="display: block; color: #00a856">
One Time Password (OTP)
</strong>
<table style="margin: 10px 0px" width="100%">
<tbody>
<tr>
<td
style="
padding: 25px;
background: #faf9f5;
border-radius: 1rem;
"
>
<strong
style="
letter-spacing: 8px;
font-size: 24px;
color: #000000;
"
>
{{.token}}
</strong>
</td>
</tr>
</tbody>
</table>
<small style="display: block; color: #6c757d; line-height: 19px">
<strong>
Please note that this is a one-time use token and it will expire
in {{.expiration}} ({{.exact}}).
</strong>
</small>
</td>
</tr>
<tr>
<td style="padding: 0px 30px">
<hr style="margin: 0" />
</td>
</tr>
<tr>
<td style="padding: 30px 30px">
<table>
<tbody>
<tr>
<td>
<strong>
Kind Regards,<br />
The John - GoAuth Team
</strong>
</td>
<td></td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</body>
</html>
{{end}}
In the file, you can see the different segments defined: subject
, plainBody
and htmlBody
. subject
is common to both plainBody
and htmlBody
. In Go, this is called Nested template definitions. With that, we can now make an instance of the Mailer
type available to our application:
// cmd/api/main.go
...
import (
...
"goauthbackend.johnowolabiidogun.dev/internal/mailer"
)
...
type config struct {
...
smtp struct {
host string
port int
username string
password string
sender string
}
}
type application struct {
..
mailer mailer.Mailer
...
}
func main() {
app := &application{
...
mailer: mailer.New(cfg.smtp.host, cfg.smtp.port, cfg.smtp.username, cfg.smtp.password, cfg.smtp.sender),
...
}
}
You need to load the SMTP server you are using via the command line or using environment variables. I used the environment variable:
// cmd/api/config.go
...
func updateConfigWithEnvVariables() (*config, error) {
...
// Email
emailPortStr := os.Getenv("EMAIL_SERVER_PORT")
emailPort, err := strconv.Atoi(emailPortStr)
if err != nil {
log.Fatal(err)
}
flag.StringVar(&cfg.smtp.host, "smtp-host", os.Getenv("EMAIL_HOST_SERVER"), "SMTP host")
flag.IntVar(&cfg.smtp.port, "smtp-port", emailPort, "SMTP port")
flag.StringVar(&cfg.smtp.username, "smtp-username", os.Getenv("EMAIL_USERNAME"), "SMTP username")
flag.StringVar(&cfg.smtp.password, "smtp-password", os.Getenv("EMAIL_PASSWORD"), "SMTP password")
flag.StringVar(&cfg.smtp.sender, "smtp-sender", "GoAuthBackend <no-reply@goauthbackend.johnowolabiidogun.dev>", "SMTP sender")
...
}
For development, I used sandbox.smtp.mailtrap.io
server and the credentials were provided by them. You can get yours by signing up. In production, however, I will be using Gmail. With that, email sending is complete!
Step 2: Activating users' accounts
If you read through our email template, we instruct users to visit a link which will be to our frontend app. On the page (we'll build this later), the user will be prompted to input the token (6-digit number), and this will be sent via a PUT
HTTP method to an endpoint, which will be created soon. This approach is more secure than prompting the user to just click a link and using a GET
request, automatically activate the account. I used that approach in the previous series which further research showed a vulnerability. We shouldn't, in the first place and obeying web standards, update a database resource via a GET
request. Our user activation endpoint looks like this:
// cmd/api/activate.go
package main
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"net/http"
"goauthbackend.johnowolabiidogun.dev/internal/tokens"
"goauthbackend.johnowolabiidogun.dev/internal/validator"
)
func (app *application) activateUserHandler(w http.ResponseWriter, r *http.Request) {
id, err := app.readIDParam(r)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
var input struct {
Secret string `json:"token"`
}
err = app.readJSON(w, r, &input)
if err != nil {
app.badRequestResponse(w, r, err)
return
}
v := validator.New()
if tokens.ValidateSecret(v, input.Secret); !v.Valid() {
app.failedValidationResponse(w, r, v.Errors)
return
}
hash, err := app.getFromRedis(fmt.Sprintf("activation_%s", id))
if err != nil {
app.badRequestResponse(w, r, err)
return
}
tokenHash := fmt.Sprintf("%x\n", sha256.Sum256([]byte(input.Secret)))
if *hash != tokenHash {
app.logger.PrintError(errors.New("the supplied token is invalid"), nil, app.config.debug)
app.failedValidationResponse(w, r, map[string]string{
"token": "The supplied token is invalid",
})
return
}
_, err = app.models.Users.ActivateUser(*id)
if err != nil {
app.serverErrorResponse(w, r, err)
return
}
ctx := context.Background()
deleted, err := app.redisClient.Del(ctx, fmt.Sprintf("activation_%s", id)).Result()
if err != nil {
app.logger.PrintError(err, map[string]string{
"key": fmt.Sprintf("activation_%s", id),
}, app.config.debug)
}
app.logger.PrintInfo(fmt.Sprintf("Token hash was deleted successfully :activation_%d", deleted), nil, app.config.debug)
app.successResponse(w, r, http.StatusOK, "Account activated successfully.")
}
This route expects an id
part of the path. We extract it using the readIDParam
method:
// cmd/api/helpers.go
...
func (app *application) readIDParam(r *http.Request) (*uuid.UUID, error) {
params := httprouter.ParamsFromContext(r.Context())
id, err := uuid.Parse(params.ByName("id"))
if err != nil {
return nil, errors.New("invalid id parameter")
}
return &id, nil
}
Then, we validated the user input, retrieved the user's token hash from redis, hashed the supplied oken and compared it with what redis returned. If they are not equal, an error is returned. If otherwise, we activated the user via the ActivateUser
method and then delete the token hash from redis. ActivateUser
method looks like this:
// internal/data/user_queries.go
...
func (um UserModel) ActivateUser(userID uuid.UUID) (*sql.Result, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `UPDATE users SET is_active = true WHERE id = $1`
result, err := um.DB.ExecContext(ctx, query, userID)
if err != nil {
return nil, err
}
return &result, nil
}
It's a very simple method that just set is_active
to true
in the database.
All the other methods including successResponse
(in cmd/api/success.go
), getFromRedis
(in cmd/api/helpers.go
), and the error responses (in cmd/api/errors.go
) are in the project's GitHub repository. Kindly copy them from there.
Now, let's register our handlers and properly give them paths in cmd/api/routes.go
:
// cmd/api/routes.go
...
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(app.notFoundResponse)
router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse)
router.HandlerFunc(http.MethodGet, "/healthcheck/", app.healthcheckHandler)
// User-related routes
router.HandlerFunc(http.MethodPost, "/users/register/", app.registerUserHandler)
router.HandlerFunc(http.MethodPut, "/users/activate/:id/", app.activateUserHandler)
return app.recoverPanic(router)
}
We registered the registerUserHandler
and activateUserHandler
. We also provided the allowed HTTP methods and in the case of activateUserHandler
, we expected an id
to be supplied as a path variable. These are the reasons we used httprouter
. If not for them, we wouldn't use it. Of course, we could entirely ditch httprouter
and write the logic ourselves but that's unnecessary as httprouter
is quite lightweight, performant, tested and trusted.
Notice that we also supplied custom responses for MethodNotAllowed
and NotFound
. This is quite convenient. As usual, the handlers are in cmd/api/errors.go
.
Let's stop here for now. In the next article, we will take a break from writing Go codes and checkout implementing the front end for the routes we currently have. Seeing what we build in action is pretty refreshing. Bye for now.
Outro
Enjoyed this article? I'm a Software Engineer and Technical Writer actively seeking new opportunities, particularly in areas related to web security, finance, health care, and education. If you think my expertise aligns with your team's needs, let's chat! You can find me on LinkedIn: LinkedIn and Twitter: Twitter.
If you found this article valuable, consider sharing it with your network to help spread the knowledge!
Top comments (0)