Authentication workflow with SSO

In this didactic article, we’ll implement a basic authentication workflow with a Single Sign-On (SSO) provider. I chose Facebook, but it should not be difficult to port the code to Google or Github. We’ll use the information returned by the SSO provider to register a user into a database. The website/app will be written in Go with the Gin framework. To summarize we will:

The final state of the code is available here.

NOTE: this is a toy example to get familiar with authentication and JWT tokens. I do not recommend using this setup in production. For production applications, I would always recommend to not implement authentication yourself, it’s not worth your time / the risk. Auth0 provides a generous free tier and a solid SDK that should allow you to get started quickly and securely.

Login and index pages

We will use two very simple HTML pages in this article:

Here is the content of index.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Simple Test</title>
    </head>
    <body>
        <h1>
            Hello, test!
        </h1>
    </body>
</html>

Here is the content of login.html:

<!DOCTYPE html>
<html>
    <head>
        <title>Login</title>
    </head>
    <body>
        <a href="/auth/facebook">Login with Facebook</a>
    </body>
</html>

Building the backend

NOTE: This article isn’t about Go per se so I’ll be brief on the Go aspect. You can find out more about Go here. If you’re not interested in the basic setup of the app, skip this section.

I will use the following structure for this project:

.
├── api
│   ├── data_registy
│   │   └── data_registry.go
│   ├── entities
│   │   └── entities.go
│   ├── handlers
│   │   └── auth
│   │       └── auth.go
│   ├── main.go
│   └── templates
│       ├── index.html
│       └── login.html
├── docker-compose.yml
├── go.mod
├── go.sum
└── README.md

Don’t worry too much if you don’t understand yet what every file is doing, I’ll detail them one by one in this article. At the moment we only need to focus on the main.go file and the templates/ directory. main.go is the entry point for the app, and templates/ contains the two pages mentioned in the section above.

In its initial form, main.go is very simple:

package main

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

func health(c *gin.Context) {
	c.JSON(http.StatusOK, "healthy")
}

func index(c *gin.Context) {
	c.HTML(http.StatusOK, "index.html", gin.H{})
}

func login(c *gin.Context) {
	c.HTML(http.StatusOK, "login.html", gin.H{})
}

func main() {
	fmt.Println("Starting")

	router := gin.Default()

	router.LoadHTMLGlob("templates/*")

	router.GET("/health", health)

	router.GET("/", index)
	router.GET("/login", login)

	router.Run("localhost:8080")
}

As it is, main.go creates a Gin app, loads the HTML templates and creates three endpoints:

To start the application, you can cd to the api directory and run:

go run .

You can also use air: it does exactly the same, but it also watches changes on the files in your directory and automatically reloads the app.

You should see something like that:

Starting
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] Loaded HTML Templates (3): 
        - 
        - index.html
        - login.html

[GIN-debug] GET    /health                   --> main.health (1 handlers)
[GIN-debug] GET    /                         --> main.index (1 handlers)
[GIN-debug] GET    /login                    --> main.login (1 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on localhost:8080

If you now head to http://localhost:8080/ in your browser you should see “Hello, test!”. You can also use curl to call an endpoint from the command line:

❯ curl "http://localhost:8080/health"
"healthy"%

At this point you should have a runnable application, without any authentication mechanism. In the second part of the article we’ll implement the authentication flow to register and login our users.

The health endpoint

This endpoint isn’t strictly necessary for a simple project, however I think it’s a good idea to always implement it. The idea behind it is simple: if the endpoint answers with a 200 HTTP code, the app is healthy. If not (if it’s a 500 for example), the app isn’t healthy. Something can call this endpoint and can decide to restart the app if it isn’t healthy. Kubernetes for example uses this mechanism to make sure an app is always healthy. A more useful version of this endpoint would be:

func health(c *gin.Context) {

	// Make sure the database is reachable
	err := data_registry.PingDB()
	if err != nil {
		c.JSON(http.StatusInternalServerError, "unhealthy")
		return
	}

	c.JSON(http.StatusOK, "healthy")
}

Facebook SSO

We will use Facebook as the Single Sign-On (SSO) provider. We’re using a SSO provider because it provides a good user experience, and it delegates the identity check to Facebook. In a nutshell, here is how it works:

For the first step, you app’s backend will have to authenticate with Facebook. You’ll have to register your app with Facebook and obtain a client ID and a client secret. The general steps are described below but you can find more information in the official documentation.

Authentication flow

Let’s get coding! We can now add this function in handlers/auth/auth.go:

package auth

import (
	"fmt"
	"net/http"
	"os"

	data_registry "local/auth_example/api/data_registy"
	ent "local/auth_example/api/entities"

	"github.com/gin-gonic/gin"
	"github.com/markbates/goth"
	"github.com/markbates/goth/gothic"
	"github.com/markbates/goth/providers/facebook"
)

// Initialize the Facebook provider for the auth flow
func init() {
	facebookProvider := facebook.New(
		os.Getenv("FACEBOOK_CLIENT_ID"),
		os.Getenv("FACEBOOK_CLIENT_SECRET"),
		os.Getenv("AUTH_REDIRECT_URL"),
	)
	goth.UseProviders(facebookProvider)
}

func Login(c *gin.Context) {
	// Insert provider into context
	// https://github.com/markbates/goth/issues/411#issuecomment-891037676
	q := c.Request.URL.Query()

	q.Add("provider", c.Param("provider"))
	c.Request.URL.RawQuery = q.Encode()

	fmt.Println("Starting auth flow")
	gothic.BeginAuthHandler(c.Writer, c.Request)
}

You will notice that the init() function is taking three parameters from the environment. This is a good development practice, because this kind of variables are likely to change between deployments (like dev, staging or production) and it allows us to decouple the code from the config, in accordance to the Twelve-Factor App principles. You can set these variables for your session by running the commands below, or you can use a .envrc file (I also recommend having a look at direnv).

export FACEBOOK_CLIENT_ID=YOUR_CLIENT_ID
export FACEBOOK_CLIENT_SECRET=YOUR_CLIENT_SECRET
export AUTH_REDIRECT_URL=http://localhost:8080/auth/callback

The new iteration of main.go looks like this:

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/*")

	router.GET("/health", health)

	router.GET("/", index)
	router.GET("/login", login)

	authGroup := router.Group("/auth")
	{
		authGroup.GET("/:provider", auth.Login)
		authGroup.GET("/callback", auth.AuthCallback)
	}

	router.Run("localhost:8080")
}

Notice this line:

authGroup.GET("/:provider", auth.Login)

This translates to: “call the auth.Login function with the provider parameter, and take this parameter from the URL”. So when the user clicks this link (from login.html), we’ll start the authentication process with the Facebook provider.

<a href="/auth/facebook">Login with Facebook</a>

When Facebook is done setting permissions, it will call our /auth/callback endpoint. Here is the function and the data structures that will take care of the query:

import (
	"time"

	"github.com/google/uuid"
	"github.com/markbates/goth"
)

func AuthCallback(c *gin.Context) {
	user, err := gothic.CompleteUserAuth(c.Writer, c.Request)

	if err != nil {
		c.AbortWithError(http.StatusUnauthorized, err)
		return
	}

	ourUser := ent.GothUser(user).ToUser()

	c.Redirect(http.StatusFound, "/")
}

type User struct {
	UserId       uuid.UUID `db:"user_id" json:"user_id"`
	CreationTime time.Time `db:"creation_time" json:"creation_time"`
	Provider     string    `db:"provider" json:"provider"`
	Email        string    `db:"email" json:"email"`
}

// Alias the non-local goth.User type so that I can implement the ToUser()
// method for it
type GothUser goth.User

func (gothUser GothUser) ToUser() User {
	return User{
		UserId:       uuid.New(),
		CreationTime: time.Now(),
		Provider:     gothUser.Provider,
		Email:        gothUser.Email,
	}
}

At this point the AuthCallback function produces a ourUser data structure that we can write to the database. I’m used to put the data structures in the entities module (because I like the clean architecture), but you can keep them in auth.go if you want.

Postgres with docker-compose

We will create a local Postgres database to store our users. These days Docker (and docker-compose) is the simplest way to spin up a database locally. To create a Postgres database, we can use the docker-compose.yml file below:

version: "3.9"
services:
  pin_db:
    container_name: auth_db
    image: postgres:14
    restart: unless-stopped
    environment:
      POSTGRES_HOST_AUTH_METHOD: "trust"
      POSTGRES_DB: auth_example
      POSTGRES_USER: postgres
    ports:
      - 5432:5432
    healthcheck:
      test: pg_isready -U postgres -d auth_example
      interval: 10s
      timeout: 3s
      retries: 3
    mem_limit: 4g
    shm_size: 1g

With these settings, we’ll create a database called auth_example and a postgres user. The connection to the database will not require a password (this is a local database, do NOT do that with a production database). The settings also implement a healthcheck, which will probe the database to make sure it’s ready to accept connections.

You can cd in the same directory as the file and run this command:

docker compose up -d

And you can then check that the database is ready with:

> docker ps

CONTAINER ID   IMAGE                    COMMAND                  CREATED         STATUS                            PORTS                                                  NAMES
5e3c78494ac4   postgres:14              "docker-entrypoint.s…"   4 seconds ago   Up 3 seconds (health: starting)   0.0.0.0:5432->5432/tcp, :::5433->5432/tcp              auth_db

Database migrations for the users table

Before we can write anything into the database, we’ll need to create the users table in the database. We could do that manually, but it’s a better practice to use schemas migrations. The idea is simple: every time you need to change the schema of your database, you create a migration up and a migration down. The up migration will bring the schema to its new state, and the down will revert the changes (useful if anything goes wrong). To manage these migrations, I like to use dbmate. It’s a super simple tool, language agnostic, and it’s not coupled with any ORM (as opposed to tools like SQLAlchemy). Let’s create our migrations:

dbmate new users_table

This will create a file db/migrations/SOME_TIMESTAMP_users_table.sql. Let’s fill it in:

-- migrate:up

create extension if not exists "uuid-ossp";

create type provider_type as enum (
    'facebook'
);

create table users (
    user_id uuid not null default uuid_generate_v4 (),
    creation_time timestamp with time zone not null default now(),
    provider provider_type not null,
    email text not null,
    primary key (user_id),
    unique (email, provider)
);

create index idx_hash_user_id on users using hash (user_id);

-- migrate:down
drop index idx_hash_user_id;

drop table users;

drop type provider_type;

Let’s detail these queries:

dbmate needs an env variable to be set to be able to reach the database, so run the following command (or use a .envrc file):

export DATABASE_URL="postgres://postgres@localhost:5433/auth_example?sslmode=disable"

We can now create the table by running:

❯ dbmate up
Applying: 20230316175233_users_table.sql
Writing: ./db/schema.sql

Congratulations! we now have a users table in the database:

+---------------+--------------------------+--------------------------------------+
| Column        | Type                     | Modifiers                            |
|---------------+--------------------------+--------------------------------------|
| user_id       | uuid                     |  not null default uuid_generate_v4() |
| creation_time | timestamp with time zone |  not null default now()              |
| provider      | provider_type            |  not null                            |
| email         | text                     |  not null                            |
+---------------+--------------------------+--------------------------------------+
Indexes:
    "users_pkey" PRIMARY KEY, btree (user_id)
    "users_email_provider_key" UNIQUE CONSTRAINT, btree (email, provider)
    "idx_hash_user_id" hash (user_id)

Data registry

I like using an abstraction layer to interact with databases, and most of the time I call it the data registry. It’s responsible for dealing with the database. Please note that it’s not a reimplementation of an ORM, it’s just an abstraction layer that decouples the application from any database logic. The rest of the application sends entities (like our User struct above) to the data registry, and the registry takes care of writing the data into the database. I’ll detail why I think it’s a good design choice in an article dedicated to the clean architecture.

package data_registry

import (
	"fmt"

	ent "local/auth_example/api/entities"

	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
)

var db sqlx.DB

// Open the DB connection and cache the query strings.
// Should be called once when the app starts.
func InitDB(connStr string) error {

	db_con, err := sqlx.Open("postgres", connStr)

	db = *db_con

	if err != nil {
		return err
	}

	if err := db.Ping(); err != nil {
		return err
	}

	return nil
}

// Make sure the DB is accessible.
// Return an error if it's not
func PingDB() error {
	if err := db.Ping(); err != nil {
		return err
	}

	return nil
}

// Upsert a user into the DB
func UpsertUser(user ent.User) error {

	fmt.Println(user)

	_, err := db.NamedExec(
		"insert into users (user_id, creation_time, provider, email) values (:user_id, :creation_time, :provider, :email) on conflict (email, provider) do nothing;",
		user,
	)

	if err != nil {
		fmt.Println(err)
		return err
	}

	return nil
}

The code above initializes a database connection and defines two simple functions:

We can now complete the AuthCallback function that we saw earlier:

func AuthCallback(c *gin.Context) {
	user, err := gothic.CompleteUserAuth(c.Writer, c.Request)

	if err != nil {
		c.AbortWithError(http.StatusUnauthorized, err)
		return
	}

	ourUser := ent.GothUser(user).ToUser()

	// Write the new user to database.
	err = data_registry.UpsertUser(ourUser)
	if err != nil {
		c.AbortWithError(http.StatusInternalServerError, err)
		return
	}

	c.Redirect(http.StatusFound, "/")
}

But let’s not forget to initialize the data registry in main.go:

func main() {

	// Crash early if connection to DB fails
	if err := data_registry.InitDB(os.Getenv("DATABASE_URL")); err != nil {
		log.Fatal("Failed to open a database connection: ", err)
	}
...
}

For the code above to work, we will need to set another env variable:

export DATABASE_URL="postgres://postgres@localhost:5432/auth_example?sslmode=disable"

Running the app

We can finally cd into the api directory and run the code!

❯ go run .
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] Loaded HTML Templates (3): 
        - 
        - index.html
        - login.html

[GIN-debug] GET    /health                   --> main.health (3 handlers)
[GIN-debug] GET    /                         --> main.index (3 handlers)
[GIN-debug] GET    /login                    --> main.login (3 handlers)
[GIN-debug] GET    /auth/:provider           --> local/auth_example/api/handlers/auth.Login (3 handlers)
[GIN-debug] GET    /auth/callback            --> local/auth_example/api/handlers/auth.AuthCallback (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on localhost:8080
Starting auth flow
[GIN] 2023/03/17 - 21:43:40 | 307 |    1.196956ms |       127.0.0.1 | GET      "/auth/facebook"
[GIN] 2023/03/17 - 21:43:41 | 302 |  486.132495ms |       127.0.0.1 | GET      "/auth/callback?code=AQBKIiXXXXXXXXXXXXXXXXXXXXXXX"
[GIN] 2023/03/17 - 21:43:41 | 200 |      88.006µs |       127.0.0.1 | GET      "/"

The console output above is what you should see if you run the app in your console and if you click the “Login with facebook” link in your browser.

Looking into the database, we can see that a new user is now registered:

+--------------------------------------+-------------------------------+----------+--------------------------------+
| user_id                              | creation_time                 | provider | email                          |
|--------------------------------------+-------------------------------+----------+--------------------------------|
| b42bf029-4423-43e9-b178-cb8972baa4ac | 2023-03-17 21:38:25.950247+00 | facebook | jeanpatrick.francoia@gmail.com |
+--------------------------------------+-------------------------------+----------+--------------------------------+

Conclusion

In this article, we saw how to create an authentication mechanism with SSO end to end. This will allow you to register/login users into your app without making them fill a form and create yet another password. But this is just a first step, because we haven’t seen the authorization part. In a future article we’ll see how we can create JWT tokens for our users, which will allow us to validate the identity of a user throughout their session on your app. We will also be able to restrict some pages to unregistered users.