Authentication system using Golang and Sveltekit - User account activation
Tags: go, webdev, tutorial
Published: 2023-06-04 09:23:10

## 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:

{% github Sirneij/go-auth %}

## 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:

```go
// 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][1] 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][2] (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
...
//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:

```go
...
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` &mdash; in this case `user_welcome.tmpl` &mdash; 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:

```html
<!-- 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][3]. With that, we can now make an instance of the `Mailer` type available to our application:

```go
// 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:

```go
// 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][4] 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:

```go
// 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:

```go
// 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:

```go
// 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`:

```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](https://www.linkedin.com/in/idogun-john-nelson/) and Twitter: [Twitter](https://twitter.com/Sirneij).

If you found this article valuable, consider sharing it with your network to help spread the knowledge!

[1]: https://pkg.go.dev/gopkg.in/gomail.v2 "Gomail"
[2]: https://golang.google.cn/pkg/embed/ "Package embed"
[3]: https://pkg.go.dev/text/template#hdr-Nested_template_definitions "Nested template definitions"
[4]: https://security.stackexchange.com/questions/197004/what-should-a-verification-email-consist-of/197005#197005 "Stackoverflow answer"