Authentication system using Golang and Sveltekit - User registration
Tags: go, webdev, tutorial, api
Published: 2023-06-04 00:25:54

## Introduction

With the basic setup laid bare, it's time to build a truly useful API service for our authentication system. In this article, we will delve into user registration, storage in the database, password hashing using [argon2id][1], sending templated emails, and generating truly random and secure tokens, among others. Let's get on!

## Source code

The source code for this series is hosted on GitHub via:

{% github Sirneij/go-auth %}

## Implementation

### Step 1: Create the user's database schema

We need a database table to store our application's users' data. To generate and migrate a schema, we'll use [golang migrate][2]. Kindly follow [these instructions][3] to install it on your Operating system. To create a pair of migration files (up and down) for our user table, issue the following command in your terminal and at the root of your project:

```shell
~/Documents/Projects/web/go-auth/go-auth-backend$ migrate create -seq -ext=.sql -dir=./migrations create_users_table
```

`-seq` instructs the CLI to use sequential numbering as against the default, which is the Unix timestamp. We opted to use `.sql` file extensions for the generated files by passing `-ext`. The generated files will live in the `migrations` folder we created in the previous article and `-dir` allows us to specify that. Lastly, we fed it with the real name of the files we want to create. You should see two files in the `migrations` folder by name. Kindly open the `up` and fill in the following schema:

```sql
-- migrations/000001_create_users_table.up.sql
-- Add up migration script here
-- User table
CREATE TABLE IF NOT EXISTS users(
    id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    email TEXT NOT NULL UNIQUE,
    password TEXT NOT NULL,
    first_name TEXT NOT NULL,
    last_name TEXT NOT NULL,
    is_active BOOLEAN DEFAULT FALSE,
    is_staff BOOLEAN DEFAULT FALSE,
    is_superuser BOOLEAN DEFAULT FALSE,
    thumbnail TEXT NULL,
    date_joined TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS users_id_email_is_active_indx ON users (id, email, is_active);
-- Create a domain for phone data type
CREATE DOMAIN phone AS TEXT CHECK(
    octet_length(VALUE) BETWEEN 1
    /*+*/
    + 8 AND 1
    /*+*/
    + 15 + 3
    AND VALUE ~ '^\+\d+$'
);
-- User details table (One-to-one relationship)
CREATE TABLE user_profile (
    id UUID NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL UNIQUE,
    phone_number phone NULL,
    birth_date DATE NULL,
    github_link TEXT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS users_detail_id_user_id ON user_profile (id, user_id);
```

In the `down` file, we should have:

```sql
-- migrations/000001_create_users_table.down.sql
-- Add down migration script here
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS user_profile;
```

We have been using these schemas right from when we started the authentication series.

Next, we need to execute the files so that those tables will be really created in our database:

```shell
migrate -path=./migrations -database=<DATABASE_URL> up
```

Ensure you replace `<DATABASE_URL>` with your real database URL. If everything goes well, your table should now be created in your database.

It should be noted that instead of manually migrating the database, we could do that automatically, at start-up, in the `main()` function.

### Step 2: Setting up our user model

To abstract away interacting with the database, we will create some sort of `model`, an equivalent of Django's model. But before then, let's create a type for our users in `internal/data/user_types.go` (create the file as it doesn't exist yet):

```go
// internal/data/user_types.go
package data

import (
    "database/sql"
    "errors"
    "time"

    "github.com/google/uuid"
    "goauthbackend.johnowolabiidogun.dev/internal/types"
)

type UserProfile struct {
    ID          *uuid.UUID     `json:"id"`
    UserID      *uuid.UUID     `json:"user_id"`
    PhoneNumber *string        `json:"phone_number"`
    BirthDate   types.NullTime `json:"birth_date"`
    GithubLink  *string        `json:"github_link"`
}

type User struct {
    ID          uuid.UUID   `json:"id"`
    Email       string      `json:"email"`
    Password    password    `json:"-"`
    FirstName   string      `json:"first_name"`
    LastName    string      `json:"last_name"`
    IsActive    bool        `json:"is_active"`
    IsStaff     bool        `json:"is_staff"`
    IsSuperuser bool        `json:"is_superuser"`
    Thumbnail   *string     `json:"thumbnail"`
    DateJoined  time.Time   `json:"date_joined"`
    Profile     UserProfile `json:"profile"`
}

type password struct {
    plaintext *string
    hash      string
}

type UserModel struct {
    DB *sql.DB
}

type UserID struct {
    Id uuid.UUID
}

var (
    ErrDuplicateEmail = errors.New("duplicate email")
)
```

These are just the basic types we'll be working on within this system. You will notice that there are three columns: names of the fields, field types, and the "renames" of the fields in JSON. The last column is very useful because, in Go, field names MUST start with capital letters for them to be accessible outside their package. The same goes to type names. Therefore, we need a way to properly send field names to requesting users and Go helps with that using the built-in `encoding/json` package. Notice also that our `Password` field was renamed to `-`. This omits that field entirely from the JSON responses it generates. How cool is that! We also defined a custom `password` type. This makes it easier to generate the hash of our users' passwords.

Then, there is this not-so-familiar `types.NullTime` in the `UserProfile` type. It was defined in `internal/types/time.go`:

```go
// internal/types/time.go
package types

import (
    "fmt"
    "reflect"
    "strings"
    "time"

    "github.com/lib/pq"
)

// NullTime is an alias for pq.NullTime data type
type NullTime pq.NullTime

// Scan implements the Scanner interface for NullTime
func (nt *NullTime) Scan(value interface{}) error {
    var t pq.NullTime
    if err := t.Scan(value); err != nil {
        return err
    }

    // if nil then make Valid false
    if reflect.TypeOf(value) == nil {
        *nt = NullTime{t.Time, false}
    } else {
        *nt = NullTime{t.Time, true}
    }

    return nil
}

// MarshalJSON for NullTime
func (nt *NullTime) MarshalJSON() ([]byte, error) {
    if !nt.Valid {
        return []byte("null"), nil
    }
    val := fmt.Sprintf("\"%s\"", nt.Time.Format(time.RFC3339))
    return []byte(val), nil
}

const dateFormat = "2006-01-02"

// UnmarshalJSON for NullTime
func (nt *NullTime) UnmarshalJSON(b []byte) error {
    t, err := time.Parse(dateFormat, strings.Replace(
        string(b),
        "\"",
        "",
        -1,
    ))

    if err != nil {
        return err
    }

    nt.Time = t
    nt.Valid = true

    return nil
}
```

The reason for this is the difficulty encountered while working with possible null values for users' birthdates. [This article][4] explains it quite well and the code above was some modification of the code there.

It should be noted that to use UUID in Go, you need an external package (we used `github.com/google/uuid` in our case, so install it with `go get github.com/google/uuid`).

Next is handling password hashing:

```go
// internal/data/user_password.go
package data

import (
    "log"

    "github.com/alexedwards/argon2id"
)

func (p *password) Set(plaintextPassword string) error {
    hash, err := argon2id.CreateHash(plaintextPassword, argon2id.DefaultParams)
    if err != nil {
        return err
    }
    p.plaintext = &plaintextPassword
    p.hash = hash
    return nil
}

func (p *password) Matches(plaintextPassword string) (bool, error) {
    match, err := argon2id.ComparePasswordAndHash(plaintextPassword, p.hash)
    if err != nil {
        log.Fatal(err)
    }

    return match, nil
}
```

We used `github.com/alexedwards/argon2id` package to assist in hashing and matching our users' passwords. It's Go's implementation of [argon2id][1]. The `Set` "method" does the hashing when a user registers whereas `Matches` confirms it when such a user wants to log in.

To validate users' inputs, a very good thing to do, we have:

```go
// internal/data/user_validation.go
package data

import "goauthbackend.johnowolabiidogun.dev/internal/validator"

func ValidateEmail(v *validator.Validator, email string) {
    v.Check(email != "", "email", "email must be provided")
    v.Check(validator.Matches(email, validator.EmailRX), "email", "email must be a valid email address")
}

func ValidatePasswordPlaintext(v *validator.Validator, password string) {
    v.Check(password != "", "password", "password must be provided")
    v.Check(len(password) >= 8, "password", "password must be at least 8 bytes long")
    v.Check(len(password) <= 72, "password", "password must not be more than 72 bytes long")
}

func ValidateUser(v *validator.Validator, user *User) {
    v.Check(user.FirstName != "", "first_name", "first name must be provided")
    v.Check(user.LastName != "", "last_name", "last name must be provided")

    ValidateEmail(v, user.Email)
    // If the plaintext password is not nil, call the standalone // ValidatePasswordPlaintext() helper.
    if user.Password.plaintext != nil {
        ValidatePasswordPlaintext(v, *user.Password.plaintext)
    }
}
```

The code uses another custom package to validate `email`, `password`, `first name`, and `last name` &mdash; the data required during registration. The custom package looks like this:

```go
// internal/validator/validator.go
package validator

import "regexp"

var EmailRX = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

type Validator struct {
    Errors map[string]string
}

// New is a helper which creates a new Validator instance with an empty errors map.
func New() *Validator {
    return &Validator{Errors: make(map[string]string)}
}

// Valid returns true if the errors map doesn't contain any entries.
func (v *Validator) Valid() bool {
    return len(v.Errors) == 0
}

// AddError adds an error message to the map (so long as no entry already exists for // the given key).
func (v *Validator) AddError(key, message string) {
    if _, exists := v.Errors[key]; !exists {
        v.Errors[key] = message
    }
}

// Check adds an error message to the map only if a validation check is not 'ok'.
func (v *Validator) Check(ok bool, key, message string) {
    if !ok {
        v.AddError(key, message)
    }
}

// In returns true if a specific value is in a list of strings.
func In(value string, list ...string) bool {
    for i := range list {
        if value == list[i] {
            return true
        }
    }
    return false
}

// Matches returns true if a string value matches a specific regexp pattern.
func Matches(value string, rx *regexp.Regexp) bool {
	return rx.MatchString(value)
}

// Unique returns true if all string values in a slice are unique.
func Unique(values []string) bool {
    uniqueValues := make(map[string]bool)
    for _, value := range values {
        uniqueValues[value] = true
    }
    return len(values) == len(uniqueValues)
}
```

Pretty easy to reason along with. 

It's finally time to create the model:

```go
// internal/data/models.go
package data

import (
    "database/sql"
    "errors"
)

var (
    ErrRecordNotFound = errors.New("a user with these details was not found")
)

type Models struct {
    Users UserModel
}

func NewModels(db *sql.DB) Models {
    return Models{
        Users: UserModel{DB: db},
    }
}
```

With this, if we have another model, all we need to do is register it in `Models` and initialize it in `NewModels`.

Now, we need to make this model accessible to our application. To do this, add `models` to our `application` type in `main.go` and initialize it inside the `main()` function:

```go
// cmd/api/main.go
...

import (
    ...
    "goauthbackend.johnowolabiidogun.dev/internal/data"
    ...
)
type application struct {
    ..
    models      data.Models
    ...
}

func main() {
    ...
    app := &application{
        ...
        models:      data.NewModels(db),
        ...
    }
    ...
}
...
```

That makes the `models` available to all route handlers and functions that implement the `application` type.

### Step 3: User registration route handler

Let's put housekeeping to good use. Create a new file, `register.go`, in `cmd/api` and make it look like this:

```go
// cmd/api/register.go
package main

import (
    "errors"
    "net/http"
    "time"

    "goauthbackend.johnowolabiidogun.dev/internal/data"
    "goauthbackend.johnowolabiidogun.dev/internal/tokens"
    "goauthbackend.johnowolabiidogun.dev/internal/validator"
)

func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
    // Expected data from the user
    var input struct {
        Email     string `json:"email"`
        FirstName string `json:"first_name"`
        LastName  string `json:"last_name"`
        Password  string `json:"password"`
    }
    // Try reading the user input to JSON
    err := app.readJSON(w, r, &input)
    if err != nil {

        app.badRequestResponse(w, r, err)
        return
    }

    user := &data.User{
        Email:     input.Email,
        FirstName: input.FirstName,
        LastName:  input.LastName,
    }

    // Hash user password
    err = user.Password.Set(input.Password)
    if err != nil {

        app.serverErrorResponse(w, r, err)
        return
    }

    // Validate the user input
    v := validator.New()
    if data.ValidateUser(v, user); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Save the user in the database
    userID, err := app.models.Users.Insert(user)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrDuplicateEmail):
            v.AddError("email", "A user with this email address already exists")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Generate 6-digit token
    otp, err := tokens.GenerateOTP()
    if err != nil {
        app.logError(r, err)
    }

    err = app.storeInRedis("activation_", otp.Hash, userID.Id, app.config.tokenExpiration.duration)
    if err != nil {
        app.logError(r, err)
    }

    now := time.Now()
    expiration := now.Add(app.config.tokenExpiration.duration)
    exact := expiration.Format(time.RFC1123)

    // Send email to user, using separate goroutine, for account activation
    app.background(func() {
        data := map[string]interface{}{
            "token":       tokens.FormatOTP(otp.Secret),
            "userID":      userID.Id,
            "frontendURL": app.config.frontendURL,
            "expiration":  app.config.tokenExpiration.durationString,
            "exact":       exact,
        }
        err = app.mailer.Send(user.Email, "user_welcome.tmpl", data)
        if err != nil {
            app.logError(r, err)
        }
        app.logger.PrintInfo("Email successfully sent.", nil, app.config.debug)
    })

    // Respond with success
    app.successResponse(
        w,
        r,
        http.StatusAccepted,
        "Your account creation was accepted successfully. Check your email address and follow the instruction to activate your account. Ensure you activate your account before the token expires",
    )
}
```

Though a bit long, reading through the lines gives you the whole idea! We expect four (4) fields from the user. After converting them to proper JSON using `readJSON`, a method created previously, we initialized the `User` type, set hash the supplied password and then validate the user-supplied data. If everything is good, we used `Insert`, a method on the `User` type that lives in `internal/data/user_queries.go`, to save the user in the database. The method is simple:

```go
// internal/data/user_queries.go
package data

import (
    "context"
    "database/sql"
    "errors"
    "log"
    "time"

    "github.com/google/uuid"
)

func (um UserModel) Insert(user *User) (*UserID, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    tx, err := um.DB.BeginTx(ctx, nil)
    if err != nil {
        return nil, err
    }

    var userID uuid.UUID

    query_user := `
    INSERT INTO users (email, password, first_name, last_name) VALUES ($1, $2, $3, $4) RETURNING id`
    args_user := []interface{}{user.Email, user.Password.hash, user.FirstName, user.LastName}

    if err := tx.QueryRowContext(ctx, query_user, args_user...).Scan(&userID); err != nil {
        switch {
        case err.Error() == `pq: duplicate key value violates unique constraint "users_email_key"`:
            return nil, ErrDuplicateEmail
        default:
            return nil, err
        }

    }

    query_user_profile := `
    INSERT INTO user_profile (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING RETURNING user_id`

    _, err = tx.ExecContext(ctx, query_user_profile, userID)

    if err != nil {
        return nil, err
    }

    if err = tx.Commit(); err != nil {
        return nil, err
    }
    id := UserID{
        Id: userID,
    }

    return &id, nil
}
```

We used Go's [database transaction][5] to execute our SQL queries. We also provided 3 seconds timeout for our database to finish up or get timed out! If the insertion query is successful, the user's ID is returned.

Next, we generated a token for the new user. The token is a random and cryptographically secure 6-digit number which then gets encoded using the sha252 algorithm. The entire logic is:

```go
// internal/tokens/utils.go
package tokens

import (
    "crypto/rand"
    "crypto/sha256"
    "fmt"
    "math/big"

    "strings"

    "goauthbackend.johnowolabiidogun.dev/internal/validator"
)

type Token struct {
    Secret string
    Hash   string
}

func GenerateOTP() (*Token, error) {
    bigInt, err := rand.Int(rand.Reader, big.NewInt(900000))
    if err != nil {
        return nil, err
    }
    sixDigitNum := bigInt.Int64() + 100000

    // Convert the integer to a string and get the first 6 characters
    sixDigitStr := fmt.Sprintf("%06d", sixDigitNum)

    token := Token{
        Secret: sixDigitStr,
    }

    hash := sha256.Sum256([]byte(token.Secret))

    token.Hash = fmt.Sprintf("%x\n", hash)

    return &token, nil
}

func FormatOTP(s string) string {
    length := len(s)
    half := length / 2
    firstHalf := s[:half]
    secondHalf := s[half:]
    words := []string{firstHalf, secondHalf}
    return strings.Join(words, " ")
}

func ValidateSecret(v *validator.Validator, secret string) {
    v.Check(secret != "", "token", "must be provided")
    v.Check(len(secret) == 6, "token", "must be 6 bytes long")
}
```

After the token generation, we temporarily store the token hash in redis using the `storeInRedis` method and then send an email, in the background using a different goroutine, to the user with instructions on how to activate their accounts. The functions used are located in `cmd/api/helpers.go`:

```go
// cmd/api/helpers.go

...
func (app *application) storeInRedis(prefix string, hash string, userID uuid.UUID, expiration time.Duration) error {
    ctx := context.Background()
    err := app.redisClient.Set(
        ctx,
        fmt.Sprintf("%s%s", prefix, userID),
        hash,
        expiration,
    ).Err()
    if err != nil {
        return err
    }

    return nil
}

func (app *application) background(fn func()) {
    app.wg.Add(1)

    go func() {

        defer app.wg.Done()
        // Recover any panic.
        defer func() {
            if err := recover(); err != nil {
                app.logger.PrintError(fmt.Errorf("%s", err), nil, app.config.debug)
            }
        }()
        // Execute the arbitrary function that we passed as the parameter.
        fn()
    }()
}
```

The tokens expire and get deleted from redis after `TOKEN_EXPIRATION` has elapsed.

I think we should stop here as this article is getting pretty long. In the next one, we will implement missing methods, configure our app for email sending and implement activating users' accounts handler. Enjoy!

## 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://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id "Argon2id"
[2]: https://github.com/golang-migrate/migrate "Database migrations. CLI and Golang library."
[3]: https://github.com/golang-migrate/migrate/tree/master/cmd/migrate "Migrate CLI Installation"
[4]: https://medium.com/aubergine-solutions/how-i-handled-null-possible-values-from-database-rows-in-golang-521fb0ee267 "The way I handled possible null values from database rows in Golang!"
[5]: https://go.dev/doc/database/execute-transactions "Executing transactions"