DEV Community

Cover image for Authentication system using Golang and Sveltekit - User account activation
John Owolabi Idogun
John Owolabi Idogun

Posted on • Updated on

Authentication system using Golang and Sveltekit - User account activation

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 logo Sirneij / go-auth

A fullstack session-based authentication system using golang and sveltekit

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
...
Enter fullscreen mode Exit fullscreen mode

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}}
Enter fullscreen mode Exit fullscreen mode

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),
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    ...
}
Enter fullscreen mode Exit fullscreen mode

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.")
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)