Build an Authentication Server

Postgres Setup

Create role & database

I am going to use PostgreSQL for this project, so let's create a database. The superuser on my computer is cfeng. If you don't have a role or wish to create a separate role for this project, then just do the following

$ psql postgres
postgres=# create role <name> superuser login;

Create a database named go_academy_userauth with owner pointing to whichever role you like. I am using cfeng on my computer.

$ psql postgres
postgres=# create database go_academy_userauth with owner=cfeng;

Actually just in case you don't remember the password to your ROLE, do the following

postgres=# alter user <your_username> with password <whatever you like>

Example

Create cfeng

postgres=# create role cfeng superuser login;

Create database

postgres=# create database go_academy_userauth with owner=cfeng;

Update password

postgres=# alter user cfeng with password 'cfeng';

Project Dependencies

I am going to introduce couple new open source libraries for this project:

  • spf13/cobra

  • sirupsen/logrus

  • labstack/echo

  • jinzhu/gorm

Project User Authentication

This project is going to use JWT based authentication instead of the traditional session based authentication. For the detailed comparison of the two, take a look at this article.

This project is not going to use real JWT for simplicity sake, instead it uses a mock token. A real token can be generated using verified open source libraries, for more information go to JWT.io.

Nevertheless, it is important to show how JWT authentication works.

Authentication Endpoints

We expose endpoints on the server for users to sign in (authenticate) or register an account. The server is responsible for returning a token to the client side. Once user receives the token, he/she will embed this token into his/her request headers when he/she needs to access resources from the user. In practice, we would have two different services to complete the whole authentication cycle but we will combine the two services into one for this project.

Authenticate

POST http://localhost:8000/api/authenticate/

Authenticate a user

Request Body

{
    "name": "Calvin Feng",
    "email": "cfeng@goacademy.com",
    "jwt_token": "qcubXgJQRIHCon0b25HnuSOgaaw="

Register

GET http://localhost:8000/api/register/

Register a new user

Request Body

{
    "id": 1,
    "name": "Calvin Feng",
    "email": "cfeng@goacademy.com",
    "jwt_token": "qcubXgJQRIHCon0b25HnuSOgaaw="
}

Once front end receives the token, it should put it into local storage. Next time when user opens his/her browser, the client application should use the same token to verify that user is indeed signed in.

User Resource Endpoints

Since we are not using real JWT token, front end needs to make requests to server to ask for user information. We need to expose some user endpoints for that.

Users

GET http://localhost:8000/api/users/

Fetch the list of all users

Headers

[
    {
        "id": 1,
        "name": "Alice",
        "email": "alice@goacademy.com"
    },
    {
        "id": 2,
        "name": "Bob",
        "email": "bob@goacademy.com"
    },
    {
        "id": 3,
        "name": "Calvin",
        "email": "calvin@goacademy.com"
    }
]

Current User

GET http://localhost:8000/api/users/current/

Fetch current user

Headers

{
    "id": 4,
    "name": "Calvin Feng",
    "email": "cfeng@goacademy.com",
    "jwt_token": "qcubXgJQRIHCon0b25HnuSOgaaw="
}

Message Resource Endpoints

Users can send messages to each other. When a user signs in to the system, he/she should be able to view all the sent and received messages.

Send Message

POST http://localhost:8000/api/messages/

Send a message to another user

Headers

Request Body

{
    "id": 3,
    "created_at": "2019-06-15T01:03:21.460369322-07:00",
    "body": "Hello there",
    "sender_id": 4,
    "receiver_id": 1
}

Users can see their own list of received and sent messages.

Sent Messages

GET http:localhost:8000/api/messages/sent/

Fetch the list of current user's sent messages

Headers

[
    {
        "id": 3,
        "created_at": "2019-06-15T01:03:21.460369-07:00",
        "body": "Hello World",
        "sender_id": 4,
        "sender": {
            "id": 4,
            "name": "Calvin",
            "email": "cfeng@goacademy.com"
        },
        "receiver_id": 1,
        "receiver": {
            "id": 1,
            "name": "Alice",
            "email": "alice@goacademy.com"
        }
    }
]

Received Messages

GET http://localhost:8000/api/messages/received/

Fetch the list of current user's received messages

Headers

[
    {
        "id": 1,
        "created_at": "2019-05-30T00:00:25.335622-07:00",
        "body": "Hello Alice",
        "sender_id": 3,
        "sender": {
            "id": 3,
            "name": "Calvin",
            "email": "cfeng@goacademy.com"
        },
        "receiver_id": 1,
        "receiver": {
            "id": 1,
            "name": "Alice",
            "email": "alice@goacademy.com"
        }
    },
    {
        "id": 2F,
        "created_at": "2019-06-15T01:03:21.460369-07:00",
        "body": "Hello World",
        "sender_id": 3,
        "sender": {
            "id": 3,
            "name": "Calvin",
            "email": "cfeng@goacademy.com"
        },
        "receiver_id": 1,
        "receiver": {
            "id": 1,
            "name": "Alice",
            "email": "alice@goacademy.com"
        }
    }
]

Models

There are two primary resources on our server, i.e. users and messages.

// User is a user model.
type User struct {
	// Both
	ID       uint   `gorm:"column:id"          json:"id"`
	Name     string `gorm:"column:name"        json:"name" `
	Email    string `gorm:"column:email"       json:"email"`
	JWTToken string `gorm:"column:jwt_token"   json:"jwt_token,omitempty"`

	// JSON only
	Password string `sql:"-" json:"password,omitempty"`

	// Database only
	CreatedAt      time.Time `gorm:"column:created_at"      json:"-"`
	UpdatedAt      time.Time `gorm:"column:updated_at"      json:"-"`
	PasswordDigest []byte    `gorm:"column:password_digest" json:"-"`
}
// Message is a model for messages.
type Message struct {
	ID        uint      `gorm:"column:id"          json:"id"`
	CreatedAt time.Time `gorm:"column:created_at"  json:"created_at"`
	UpdatedAt time.Time `gorm:"column:updated_at"  json:"-"`
	Body      string    `gorm:"column:body"        json:"body"`

	// Foreign keys
	SenderID   uint  `gorm:"column:sender_id"       json:"sender_id"`
	Sender     *User `gorm:"foreignkey:sender_id"   json:"sender,omitempty"`
	ReceiverID uint  `gorm:"column:receiver_id"     json:"receiver_id"`
	Receiver   *User `gorm:"foreignkey:receiver_id" json:"receiver,omitempty"`
}

Migrations

Instead of using auto migration feature of GORM, I prefer to write our own SQL because it gives us greater flexibility and better organization.

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP WITH TIME ZONE,
    updated_at TIMESTAMP WITH TIME ZONE,
    name VARCHAR(255),
    email VARCHAR(255),
    jwt_token VARCHAR(255),
    password_digest BYTEA
);

CREATE UNIQUE INDEX ON users(name);
CREATE UNIQUE INDEX on users(email);
CREATE UNIQUE INDEX ON users(jwt_token);

CREATE TABLE messages (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP WITH TIME ZONE,
    updated_at TIMESTAMP WITH TIME ZONE,
    sender_id INTEGER REFERENCES users(id),
    receiver_id INTEGER REFERENCES users(id),
    body TEXT
);

CREATE INDEX ON messages(sender_id);
CREATE INDEX ON messages(receiver_id);

Put everything together

Let's start off with creating a skeleton for the project. Details will be discussed in the video section. Create a project file structure as follows.

go-academy/
    userauth/
        cmd/
            run_migrations.go
            run_server.go
        handler/
        migrations/
        model/
        public/
        main.go

Inside the main.go, set up logging and cobra commands.

main.go
package main

import (
	"os"

	"github.com/calvinfeng/go-academy/userauth/cmd"

	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
)

func main() {
	logrus.SetFormatter(&logrus.TextFormatter{
		FullTimestamp: true,
	})

	logrus.SetOutput(os.Stdout)

	logrus.SetLevel(logrus.DebugLevel)

	root := &cobra.Command{
		Use:   "userauth",
		Short: "user authentication service",
	}

	root.AddCommand(cmd.RunMigrationsCmd, cmd.RunServerCmd)
	if err := root.Execute(); err != nil {
		logrus.Fatal(err)
	}
}

Create two commands inside cmd package, one for migration and one for running server.

run_server.go
package cmd

import (
	"io"
	"net/http"
	"os"

	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/jinzhu/gorm"
	"github.com/spf13/cobra"

	// Driver for Postgres
	_ "github.com/jinzhu/gorm/dialects/postgres"
)

// RunServerCmd is a command to run server from terminal.
var RunServerCmd = &cobra.Command{
	Use:   "runserver",
	Short: "run user authentication server",
	RunE:  runServer,
}

func runServer(cmd *cobra.Command, args []string) error {
	_, err := gorm.Open("postgres", pgAddr)
	if err != nil {
		return err
	}

	srv := echo.New()

	srv.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
		Format: "HTTP[${time_rfc3339}] ${method} ${path} status=${status} latency=${latency_human}\n",
		Output: io.MultiWriter(os.Stdout),
	}))

	srv.Use(middleware.CORSWithConfig(middleware.CORSConfig{
		AllowOrigins: []string{"*"},
		AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
	}))

	srv.File("/", "public/index.html")
	srv.Static("/assets", "public/assets")
	if err := srv.Start(":8080"); err != nil {
		return err
	}

	return nil
}
package cmd

import (
	"fmt"

	"github.com/golang-migrate/migrate"
	"github.com/sirupsen/logrus"
	"github.com/spf13/cobra"
	
	_ "github.com/lib/pq" // Driver
	_ "github.com/golang-migrate/migrate/database/postgres" // Driver
	_ "github.com/golang-migrate/migrate/source/file"       // Driver
)

const (
	host         = "localhost"
	port         = "5432"
	user         = "cfeng"
	password     = "cfeng"
	database     = "go_academy_userauth"
	ssl          = "sslmode=disable"
	migrationDir = "file://./migrations/"
)

var log = logrus.WithFields(logrus.Fields{
	"pkg": "cmd",
})

var pgAddr = fmt.Sprintf("postgresql://%s:%s@%s:%s/%s?%s", user, password, host, port, database, ssl)

// RunMigrationsCmd is a command to run migration.
var RunMigrationsCmd = &cobra.Command{
	Use:   "runmigrations",
	Short: "run migration on database",
	RunE:  runMigrations,
}

func runMigrations(cmd *cobra.Command, args []string) error {
	migration, err := migrate.New(migrationDir, pgAddr)
	if err != nil {
		return err
	}

	log.Info("performing reset on database")
	if err = migration.Drop(); err != nil {
		return err
	}

	if err := migration.Up(); err != nil {
		return err
	}

	log.Info("migration has been performed successfully")
	return nil
}

Video

Now your project should be able to compile and you should be able to run migration on the database.

go install && userauth runmigrations

In the video, I will discuss how to write each endpoint.

Additional Resource

If you want to learn more about session storage, security, encryption, and many other topics relating to web applications, take a look at this GitBook.

Source

GitHub

Last updated