How to implement a basic CRUD in Golang protected by Auth0
Tags: go, auth0, security, tutorial
Published: 2022-08-17 11:42:47

## About

This tutorial is a step-by-step guide for implementation of a system capable of restricting access to resources for users, which doesn't have sufficient rights for these particular resources. You'll understand some key concepts and build such system from scratch by yourself.

If you already understand these mechanisms and just looking for a quick solution, feel free to fork [this](https://github.com/MrSydar/apiserver-example) project.

## Prerequisites

This tutorial assumes that:

📌 You are capable of writing a basic CRUD application in Go. Here we will use [Echo](https://echo.labstack.com/) framework for API creation and [MongoDB](https://www.mongodb.com/) for storage. However, it is enough to have understanding how to create such system using these technologies.

📌 You know what is JWT. Is not, please watch [this](https://www.youtube.com/watch?v=7Q17ubqLfaM&ab_channel=WebDevSimplified) amazing video. 

## Authorization 👮

Let's say that we want to build a service, which would allow users to store their private notes. In our example, a note will be just a text string.

To control access to such a vulnerable data, we should know who is its owner. To do that, we will store an owner identifier along with each data record, so when some user tries to get a note, we could compare his identifier with the identifier that is stored along the note. If they are equal, then we could safely provide the requested data.

## Authentication 🔐

To authenticate a user, we will use `Auth0` as it allows us to concentrate on features of our application instead of securing vulnerable user data such as credentials. It also provides some neat features as authentication with Google account and free tier with up to 7k users which is more than enough for your MVP 😎

Let's do some basic setup and discuss authentication concept.

### Create Auth0 application

First things first, you need an account on the `Auth0` platform, so head to their [page](https://auth0.com/) and create one.

After that, go to the `dashboard`, find `Applications` page and click on `Create Application`:
![New application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lpce7ob5sgkcmwr0uo2u.png)
 
Name is whatever you like, select `Regular Web Application` and click `Create`. You should be redirected to the page of the newly created application.

![Application page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fo6ao7fke4tm4uportyn.png)

You can find here the `Domain`, `Client ID` and `Client Secret`. Remember where you can find these values, as we will need them in the future.

Now we need to set it up to use `HS256` signing algorithm, so we could verify incoming JWT tokens from users using our application secret. In this way, our API server could authenticate the user making a request and make sure that data provided inside the token is valid.

Go to `Settings`, scroll down to the `Application URIs` section and add `http://localhost:9000/callback` URL to the text field under the `Allowed Callback URLs` title.

![Application URIs](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cr2nis18879z57aceefe.png) 

After that, continue scrolling down to the `Advanced settings` button and click on it. Next, go to `OAuth` page and in the `JSON Web Token (JWT) Signature Algorithm` section, select `HS256`.

![Advanced settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ceaodd86v70ytiqih1li.png)

Finally, save the changes by clicking `Save Changes` button at the very bottom of the page.

![Save Changes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tfkafylbu256sgmoryxg.png)

### Concept

To get access to the requested resource, the user must prove that he is truly the person it claims to be. Therefore, we need a JWT signed by our application, so we could verify the data inside the provided token with our secret.

To get such token, the user should land on our application login page managed by `Auth0`. Then he gets authenticated using whatever way he likes and in case of success, he gets a special `code`. Next, our API server should use this value along with application `secret` and other data to generate a JWT token which belongs to our user. To make this happen, the `Auth0` login page redirects the user client to the callback URL we provided with `code` value stored in the URL query parameters under, well, the `code` name. After that, the API server reads the `code`, sends a `POST` request to `Auth0`, gets a token and provides a JWT to the user client.

You can play around with this flow manually by following the steps on this [page](https://auth0.com/docs/get-started/authentication-and-authorization-flow/add-login-auth-code-flow#authorize-user).

## Create MongoDB database

Here we will create MongoDB Atlas database as it's easy to set up and free. You might want to skip this step if you want to use your database, but for folks that need some help, this section would be handy 🙌

As always, create an account on [mongodb.com](https://account.mongodb.com/account/login) and log in to it. Next, head to `Projects` page and create a new project by pressing `New Project` button:

![Projects](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i12s5n1ljf4lnf79q7z2.png)

Provide any name:

![Project name](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3s7lpsz8z4h542wj24nh.png)

Press `Create Project` button:

![Create Project](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6qx2qi6iqyg1i8t1tdgf.png)

Next, go to the `Database` page and press `Build a Database` button.

![Build a Database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q662j18c8o1m03buh5eg.png) 

To create a free database, choose "Shared" plan by pressing `Create` button under its label.

![Shared plan](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iw2e1wyw3905k3h7cp2t.png)

On the next page, you can set up your free cluster if you want, but for purposes of this guide a default configuration is enough. Let's change the name and press `Create Cluster`.

![Cluster Settings](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j5yqqyb4xjmeq4xlo148.png)

The `Security Quickstart` page should open. Here, we need to set up access to the newly created cluster.

Firstly, let's create credentials an API server is going to use to exchange data with the database. You can enter any username and password you want. For the example, I am going to type `admin` to both fields. Please remember these values as we are going to use them later. Press `Create User` button.

![User creation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xruvdpbipxadxdqq0r6v.png)

In the section below, we should provide an IP address from which the database could be accessible. Most likely, you are going to run the API server from the same machine you're registering a cluster from your browser. Press `Add My Current IP Address` if this is the case. Otherwise… you got the idea 😉

![Add IP address](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oemb0w00502cj2w1mde4.png)

On the `Database` page that will open, find created cluster and press `Connect` button. Next, click `Connect your application`.

![Connect your application](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l22ai5yvoizt34xarudj.png)

Select `Go` as driver, choose your version and remember the mongo URI that was generated for you. API server will use it to establish connection with the database.

![mongo URI](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jl3hryz6doficn4hq716.png)

That's it! Now we set and ready to finally start coding 👨‍💻 

## Create an API server

Okay, enough theory, let's move to practice 😉

### Initialization

Create a directory for our project, run `go mod init` to initialize it and create a `main.go` file with the next imports:

```golang
package main

import (
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
)
```

Define the `main()` function and set up a basic `echo` application. Let's make it neat and add a `logger` and `recover` middleware.

```golang
func main() {
	e := echo.New()

	if err := godotenv.Load(); err != nil {
		panic(fmt.Errorf("failed to load .env file: %v", err))
	}

	e.Use(middleware.Recover()).Find
	e.Use(middleware.Logger())

	e.Logger.Fatal(e.Start(":9000"))
}
```

Next, create a `.env` file and fill it with data from previous steps of this guide. Don't forget to replace the `<password>` value in the mongo URI.

```
DATABASE_NAME='example-database'
MONGO_URI='mongodb+srv://admin:admin@example-cluster.f1wmqog.mongodb.net/?retryWrites=true&w=majority'

AUTH0_DOMAIN='dev-tslb5vli.us.auth0.com'
AUTH0_CALLBACK_URL='http://localhost:9000/callback'
AUTH0_CALLBACK_ENDPOINT='/callback'
AUTH0_CLIENT_ID='PpstQbfS9tDdQvSTjDVbzRe8QWmGplrA'
AUTH0_CLIENT_SECRET='UKRAAKSJDFAfds4v1FXGGGDx66j343asd24FSASAMLhAEecJdasdkKJK3k3_V7CkdlksdtINE'
```

### Connect to the database

Create a `configs` directory with the `database.go` file inside. Provide package name and add the next imports:

```golang
package configs

import (
	"go.mongodb.org/mongo-driver/mongo"
	"go.mongodb.org/mongo-driver/mongo/options"
)
```

Next, we need to define a function used to establish a connection with the database.

```golang
...

func connectDB() (*mongo.Client, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	mongoURI := os.Getenv("MONGO_URI")
	client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
	if err != nil {
		return nil, fmt.Errorf("failed to create a new client: %v", err)
	}

	err = client.Ping(ctx, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to ping the database: %v", err)
	}

	return client, nil
}
```

Then, add some global variables to this config for the database client and collections that will be explained later in this guide:

```golang
var DB *mongo.Client
var Collections struct {
	Accounts *mongo.Collection
	Notes *mongo.Collection
}
```

Finally, we need to initialize them. Create `init` function:

```golang
...

func init() {
	log.Print("Initializing database connection")

	var err error
	DB, err = connectDB()
	if err != nil {
		return log.Fatalf("failed to connect to database: %v", err)
	}

	databaseName := os.Getenv(envnames.DatabaseName)
	Collections.Accounts = DB.Database(databaseName).Collection("accounts")
	Collections.Notes = DB.Database(databaseName).Collection("notes")
}
```

Import this package in the `main.go` to call initialization:

```golang
package main

import (
	_ "<your-module-name>/configs"
	
	...
```

More info about that kind of import could be found in the official [specification](https://go.dev/ref/spec#Import_declarations) page. BTW, I find the specification page to be the best source of information regarding Golang. Seriously, invest some time and read it 📖

### Create a callback endpoint

Now let's add a nice callback controller with some error handling. To do so, create a `controllers` directory with a file `callback.go` inside of it and define a function to fetch JWT from the `Auth0` service. Let's call it `FetchJWT`. It should read the `code` URL parameter that user client will pass after Auth0 server will redirect him to our endpoint.

```golang
func FetchJWT(c echo.Context) error {
	code := c.QueryParam("code")
	if code == "" {
		return c.String(http.StatusBadRequest, "code parameter was not provided")
	}
	
	...
```

Next, we need to send a `POST` request with this code to `Auth0` and verify a response:

```golang
	...

	tokenFetchURL = fmt.Sprintf("https://%s/oauth/token", os.Getenv("AUTH0_DOMAIN"))

	data := url.Values{
		"grant_type":    {"authorization_code"},
		"client_id":     {os.Getenv("AUTH0_CLIENT_ID")},
		"client_secret": {os.Getenv("AUTH0_CLIENT_SECRET")},
		"redirect_uri":  {os.Getenv("AUTH0_CALLBACK_URL")},
		"code":{[]string{code}},
	}

	response, err := http.PostForm(tokenFetchURL, data)
	if err != nil {
		return fmt.Errorf("failed to retrieve JWT token from Auth0 server: %v", err)
	}

	body, err := io.ReadAll(response.Body)
	if err != nil {
		return fmt.Errorf("failed to read response form Auth0 server: %v", err)
	}

	if response.StatusCode != http.StatusOK {
		if response.StatusCode == http.StatusForbidden {
			return responses.Message(c, http.StatusForbidden, "got response from auth0: unauthorized")
		} else {
			return fmt.Errorf("got bad response from auth0: %v", err)
		}
	}
	
	...
```

We want to make sure that every verified JWT to our API server will contain user email. We will use that value as an identifier for internal user account in our application (more about that later). For now, let's create a check to ensure that every token we return to our users contains an "email" scope, which basically implies that one of the JWT claims contain user email. 

```golang
	...

	fieldsToCheck := struct {
		Scope string `json:"scope"`
	}{}

	if err := json.Unmarshal(body, &fieldsToCheck); err != nil {
		return fmt.Errorf("failed to unmarshal body for field check: %v", err)
	}

	if !strings.Contains(fieldsToCheck.Scope, "email") {
		return responses.Message(c, http.StatusBadRequest, `"email" scope is required`)
	}

	return c.String(http.StatusOK, string(body))
}
```

Finally, let's register the callback endpoint on our server. As always, we are trying to stick to the best practices to make everything nice and readable. Create `routes` directory along with the file `callback.go` inside. Paste the code below (you would probably need to change the imports):

```golang
package routes

import (
	"mrsydar/apiserver/controllers"

	"github.com/labstack/echo/v4"
)

func ApplyCallback(e *echo.Echo) {
	e.GET(os.Getenv("AUTH0_CALLBACK_ENDPOINT"), controllers.FetchJWT)
}
```

Now apply this endpoint in the `main` function:

```golang
	...
		
	e.Use(middleware.Logger())

	routes.ApplyCallback(e)

	e.Logger.Fatal(e.Start(":9000"))
	
	...
```

Let's test it! Run the server with `go run .`. Fill in the required values and open the next URL:

```
https://<AUTH0_DOMAIN>/authorize?response_type=code&client_id=<AUTH0_CLIENT_ID>&redirect_uri=http://localhost:9000/callback&scope=openid%20email&state=STATE
```

In my case it looks in the next way:

```
https://dev-tslb5vli.us.auth0.com/authorize?response_type=code&client_id=PpstQbfS9tDdQvSTjDVbzRe8QWmGplrA&redirect_uri=http://localhost:9000/callback&scope=openid%20email&state=STATE
```

You should see the login page that Auth0 made for us:

![Auth0 login page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0pydclozcu7z1kfr8xho.png)

Authorize yourself by clicking `Accept`. You will be automatically redirected to our callback endpoint `http://localhost:9000/callback`. Our backend will read the `code`, do the JWT request and return the result to you. Normally, this should be handled by frontend of our project, but it's enough as long as it works 👀

![Callback page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mr1d1oammsuwh8u1lgac.png)  

Voilà! We are done with the callback. Cool 😎

### Create internal accounts

Let's have a look into our JWT token. To do that, copy the value from `id_token` field of the callback response and paste it into the [jwt.io](https://jwt.io/) page. One of the token claims contains an `email` value. We can use this value to protect our data in the DB.

For example, let's go back to our example with notes. Each `note` record in our database should contain 3 fields:
* `_id` of type `objectId`
* `data` of type `string`
* `owner_email` of type `string`

Now you got it ?

When user will send a request to list his notes, we will validate his token and then return all the notes which have `owner_email` set to `email` value from JWT. Genius and simple!

But there are a lot more benefits to create and store internal user accounts in your database and use their identifiers instead of just using an `email` field. So let's do that.

First things first, define a model for our accounts. Create a `models` directory and `account.go` inside. Define account model there:

```golang
package models

import "go.mongodb.org/mongo-driver/bson/primitive"

type Account struct {
	ID primitive.ObjectID `bson:"_id,omitempty"`

	Email string `bson:"email,omitempty" validate:"required"`
}
```

We need to write our own middleware which will create and associate the internal user with the incoming request. Create a `middlewares` directory with `accounts.go` inside. Define a AssociateAccountWithRequest middleware function.

```golang
func AssociateAccountWithRequest(next echo.HandlerFunc) echo.HandlerFunc {
	func (next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			return next(c)
		}
	}
}
```

Next, read an email stored inside the JWT. To do so, paste the next piece of code inside the anonymous function we defined inside the `AssociateAccountWithRequest` right before the `next(c)` call:

```golang
		...
		
		user := c.Get("user").(*jwt.Token)
		claims := user.Claims.(jwt.MapClaims)
		email := claims["email"].(string)
		if email == "" {
			return errors.New("email value is empty in the received JWT token")
		}
		
		...

```

Now, fetch the account from our DB by the `email` value, or create the new one:

```golang
		...

		account := models.Account{}
		if err := database.Collections.Accounts.FindOne(context.Background(), bson.M{"email": email}).Decode(&account); err != nil {
			if err == mongo.ErrNoDocuments {
				account.Email = email
				result, err := database.Collections.Accounts.InsertOne(context.Background(), account)
				if err != nil {
					return fmt.Errorf("failed to insert account resource: %v", err)
				}
				account.ID = result.InsertedID.(primitive.ObjectID)
			} else {
				return fmt.Errorf("failed to get account: %v", err)
			}
		}

		...
```

Finally, let's pass an account ID to the request context, so the controller could return only data accessible to the user sending a request.

```golang
	...
	c.Set(contextnames.AccountID, account.ID)
	
	return next(c)
	}
}
```

Let's summarize. We have just created a middleware which we could use on any route for our API server. It will verify the user sending a request and create or get an already existing internal user and pass its identifier to the next steps of the request pipeline. Fabulous 🦄

### Create notes controller

Now, when the middleware is ready, go ahead and create two endpoints to post and list data, which needs to be protected. In our case, this is a `note` object.

Foremost, define a `note` model the `note.go` in the `models` directory. Add `ID` field, `Owher` field to tie the note to some owner (i.e. internal user) and the `text` field to the data itself.

```golang
package models

type Note struct {
	ID    primitive.ObjectID `bson:"_id" json:"_id"`
	Owner primitive.ObjectID `bson:"owner,omitempty" json:"owner"`

	Text string `bson:"text" json:"text"`
}
```

The model is ready, now we need some controllers to put it in action. Create a `note.go` file inside the `controllers` directory.

Define a function which would allow users to store their notes. It should parse the request body and insert the received data to the appropriate documents collection:

```golang
func PostNote(c echo.Context) error {
	var note models.Note

	body, err := io.ReadAll(c.Request().Body)
	if err != nil {
		msg := "failed to post a note"
		log.Logger.Errorf("%v: %v", msg, err)
		return responses.Message(c, http.StatusBadRequest, msg)
	}

	if len(body) == 0 {
		msg := "request body is empty"
		return responses.Message(c, http.StatusBadRequest, msg)
	}

	if err := json.Unmarshal(body, &note); err != nil {
		msg := "failed to post a note"
		log.Logger.Errorf("%v: %v", msg, err)
		return responses.Message(c, http.StatusInternalServerError, msg)
	}

	note.ID = primitive.NewObjectID()
	note.Owner = c.Get("accountID").(primitive.ObjectID)

	result, err := database.Collections.Notes.InsertOne(context.Background(), note)
	if err != nil {
		msg := "failed to post a note"
		log.Logger.Errorf("%v: %v", msg, err)
		return responses.Message(c, http.StatusInternalServerError, msg)
	}

	note.ID = result.InsertedID.(primitive.ObjectID)

	return c.JSON(http.StatusCreated, note)
}
```

Now define a function to fetch notes by owner:

```golang
func GetNotes(c echo.Context) error {
	notes := []models.Note{}

	accountID := c.Get("accountID").(primitive.ObjectID)
	cur, err := database.Collections.Notes.Find(context.Background(), bson.M{"owner": accountID})
	if err != nil {
		msg := "failed to list notes"
		log.Logger.Errorf("%v: %v", msg, err)
		return responses.Message(c, http.StatusInternalServerError, msg)
	}

	if err := cur.All(context.Background(), &notes); err != nil {
		msg := "failed to list notes"
		log.Logger.Errorf("%v: %v", msg, err)
		return responses.Message(c, http.StatusInternalServerError, msg)
	}

	return c.JSON(http.StatusOK, notes)
}
```

Finally, tie these functions to appropriate endpoints in the `note.go` file inside the `routes` directory. Don't forget to use the middleware we created. Otherwise, a request context won't contain the necessary owner identifier.

```golang
func ApplyNotes(e *echo.Echo) {
	e.GET("/notes", controllers.GetNotes,
		middleware.JWTWithConfig(middleware.JWTConfig{SigningKey: []byte(os.Getenv("AUTH0_CLIENT_SECRET"))}),
		d2middlewares.AssociateAccountWithRequest,
	)

	e.POST("/notes", controllers.PostNote,
		middleware.JWTWithConfig(middleware.JWTConfig{SigningKey: []byte(os.Getenv("AUTH0_CLIENT_SECRET"))}),
		d2middlewares.AssociateAccountWithRequest,
	)
}
```

Apply these routes in the `main` function:

```golang
	...

	routes.ApplyCallback(e)
	routes.ApplyNotes(e)

	e.Logger.Fatal(e.Start(":9000"))
}
```

Here we are, the server is ready to be tested 🌟

## Test it 🧪

Start our server with `go run .` command. It should connect to the database and start serving our endpoints.

Now, send a `POST` request to the `localhost:9000/notes` with the body containing any text (e.g. `{"text":"vulnerable data"}`). Response should contain our note populated with `_id` and `owner` data. Don't forget to add `Authorization` header and put the JWT we retrieved previously `Bearer <id_token value>`. Something like: `Authorization: Bearer eyJhbGciOiJIUzI1NiIsR5cCI6IkpXVCJ9...`. Note, that token has an expiration time. If it got expired, you need to repeat the authentication process we did earlier and copy a new `id_token` (there's also an option to refresh tokens, more on that you could find in the Auth0 [docs](https://auth0.com/docs/secure/tokens/refresh-tokens)).

![POST note](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/v3jb71t5uudyb7ntampi.png)

Let's send 2 notes to the server.

Next, list them with the `GET` request to the same endpoint. Again, don't forget to set JWT as previously.

![List notes](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4nmyvilzmyz5f3pfszk8.png)

Ta-da 🎉

If something doesn't work, don't worry. Just have a look into the ready [project](https://github.com/MrSydar/apiserver-example).

## Homework

Sign up to our application with another account, put some notes and then list them. You are expected to see notes only for the new user. This is what we wanted to achieve.

Also, look through the database collections and make sure you understand each field there:

![collections](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qa8jkrgic4gz0dmzm9r2.png)

## Afterword

You did it! Congratulations 🥳