Role-based Access Control in Golang with jwt-go
Tags: devsecops, go, apisec, rbac
Published: 2023-06-20 18:46:50

As a security engineer, what's the first thing you check when testing a web or mobile application? Access control misconfigurations must be at the top of your list if you like going for the low hanging fruits first. 

Surprisingly, access control is one of the components that application developers think will be simple, but ends up taking much of their time with no end user value, and even worse ends being poorly done, tremendous exposing the application to security risks. It is no coincidence that Authentication and Authorization related issues take the first two positions in the [OWASP Top 10 - 2023](https://owasp.org/API-Security/editions/2023/en/0x11-t10//) list of common API security risks.

For this reason, in this article I am going to illustrate the application of Role-based Access Control; a simple and yet secure access control approach for enterprise APIs.

**Role-based Access Control (RBAC)**
[Role-based Access Control](https://auth0.com/docs/manage-users/access-control/rbac) is a mechanism that restricts access to an information system based on user job functions or roles. Access to data is restricted through permissions and privileges that are attached to different user roles. 

**Let's Build**
By the end of this article we are going build a simple room booking API with [Go](https://go.dev/) that will allow visitors to view available rooms as well as register, login and book rooms. There will also be an administrator who will have the permissions map user to permissions as well as add and update rooms information.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cmhbz445uqftysbg9zq2.png)

**Prerequisites**
- Basic knowledge of Go
- Patience 😄

**Source Code**
```go
$ git clone https://github.com/bensonmacharia/jwt-go-rbac.git
```

_**Step 1: Setting up the environment**_

- Get your favourite IDE, mine is [vscode](https://code.visualstudio.com/)
- Download and install [Go](https://go.dev/doc/install)
- Confirm installed go version

```go
$ go version    
go version go1.19.4 darwin/arm64
```
- [Install MySQL](https://dev.mysql.com/doc/mysql-installation-excerpt/5.7/en/) and test connection
```go
// confirm installation by checking version
$ mysql --version
mysql  Ver 8.0.32 for macos12.6 on arm64 (Homebrew)

// test connection
$ mysql -u bmacharia -p
Enter password: 
mysql>
```
- Create a database
```javascript
// create database
$ mysql> CREATE DATABASE jwt_go_rbac;
Query OK, 1 row affected (0.07 sec)

// confirm database creation
$ mysql> show databases;
+-------------------------+
| Database                |
+-------------------------+             |
| information_schema      |
| jwt_go_rbac             |
| mysql                   |
| performance_schema      |
| sys                     |
+-------------------------+
5 rows in set (0.01 sec)
```
**_Step 2: Setting up the project_**

- Create project folder and initialise go project

```go
$ mkdir jwt-go-rbac                                                                
$ cd jwt-go-rbac            
$ go mod init bmacharia/jwt-go-rbac            
go: creating new go.mod: module bmacharia/jwt-go-rbac
```
- Create the main.go and .env files

```go
// main go application file
$ touch main.go

// environment variables configuration file
$ touch .env
```
- Open the project in your IDE and edit the .env file as below

{% gist https://gist.github.com/bensonmacharia/8fbb4d17313016ac0eaff6ad577b336d %}

**_Step 3: Configure the database connection_**
- Install required packages

```go
// package to load .env file
$ go get github.com/joho/godotenv
// package to allow connection to MySQL database
$ go get -u gorm.io/driver/mysql
// a simple Go HTTP web framework
$ go get github.com/gin-gonic/gin
// ORM (Object Relational Mapping) library for Go
$ go get -u gorm.io/gorm
// jwt-go package
$ go get -u github.com/golang-jwt/jwt/v5
```
- Create a database connection file

```go
// create a database folder and a file inside it
$ mkdir database
$ touch database/database.go
```
- Edit the database.go file as below. We are using GORM to initiate a connection to MySQL database.

{% gist https://gist.github.com/bensonmacharia/d728119b4cd7d304a0a2ed3c88f88bce %}

- Edit the main.go file to test database connection. In this file we are loading the .env file, database connection and starting the gin web server on port 8000
{% gist https://gist.github.com/bensonmacharia/329c8996826e91ff8368e87f9aafbf55 %}

- Test database connection by running the `go run` command
```go
$ go run main.go
2023/05/01 23:25:13 .env file loaded successfully
2023/05/01 23:25:13 `Successfully connected to the database
[GIN-debug] Listening and serving HTTP on :8000
```

**_Step 4: Create database models_**
- Create the user and role models. The user model defines the user object details as well as user properties in the database. On the other hand, the role model details properties about a role to be assigned to each user.
```go
// create a user.go file inside the model directory
$ mkdir model
$ touch model/user.go
$ touch model/role.go
```
- model/user.go
{% gist https://gist.github.com/bensonmacharia/c8151277d7a0a1c3bb5b43e971cb90e9 %}
- model/role.go
{% gist https://gist.github.com/bensonmacharia/6d632b85b27bdd0d7d0cb22e51d62a23 %}
- Run database migrations
```go
//edit main.go file to add automigration script
...
// run database migrations and add seed data
func loadDatabase() {
	database.InitDb()
	database.Db.AutoMigrate(&model.Role{})
	database.Db.AutoMigrate(&model.User{})
	seedData()
}

// load seed data into the database
func seedData() {
	var roles = []model.Role{{Name: "admin", Description: "Administrator role"}, {Name: "customer", Description: "Authenticated customer role"}, {Name: "anonymous", Description: "Unauthenticated customer role"}}
	var user = []model.User{{Username: os.Getenv("ADMIN_USERNAME"), Email: os.Getenv("ADMIN_EMAIL"), Password: os.Getenv("ADMIN_PASSWORD"), RoleID: 1}}
	database.Db.Save(&roles)
	database.Db.Save(&user)
}
// run migration
$ go run main.go
..
```
- Confirm that users and roles tables have been created
```sql
mysql> show tables;
+-----------------------+
| Tables_in_jwt_go_rbac |
+-----------------------+
| roles                 |
| users                 |
+-----------------------+
2 rows in set (0.01 sec)
mysql> desc users;
mysql> desc roles;
mysql> select * from roles;
mysql> select * from users;
```
- Create models for user registration, login and update
```go
$ touch model/register.go
$ touch model/login.go
$ touch model/update.go
```
- model/register.go
{% gist https://gist.github.com/bensonmacharia/df6d4028823be0ce9129101db14abd01 %}
- model/login.go
{% gist
https://gist.github.com/bensonmacharia/013e11de0da854ea4e87b3162a13ddb8 %}
- model/update.go
{% gist
https://gist.github.com/bensonmacharia/27d50bf4d77a1b172c532b5e034af4be %}

**_Step 5: Create controllers to interact with database content_**
- Create the role and user controller files
```go
$ touch controller/role.go
$ touch controller/user.go
```
- controller/role.go
{% gist
https://gist.github.com/bensonmacharia/6fe6efc937bde705f687a35a66fec53e %}
- controller/user.go
{% gist
https://gist.github.com/bensonmacharia/3152d18066b446d45fdbb650f1eea9c2 %}
- Add registration and login routes
```go
// edit main.go
func serveApplication() {
	router := gin.Default()
	authRoutes := router.Group("/auth/user")
        // registration route
	authRoutes.POST("/register", controller.Register)
        // login route
	authRoutes.POST("/login", controller.Login)

	router.Run(":8000")
	fmt.Println("Server running on port 8000")
}
```

- Test user registration and login
```go
// run the application
$ go run main.go
// register user
$ curl -X POST http://localhost:8000/auth/user/register \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"username": "test","email":"test@bmacharia.com","password":"super^Secret!007"}'
// test user login
$ curl -X POST http://localhost:8000/auth/user/login \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"username": "test","password":"super^Secret!007"}' 
```

_**Step 6. Add JWT utility for token creation and validation**_
- To create a secure Role-based authentication scheme, we need to generate a unique token when the user authenticates. This is then used to track their assigned role as they consume the availed resources. In this project we are going to use the [jwt-go](https://github.com/golang-jwt/jwt) package to generate a [JWT](https://jwt.io/introduction) token that will encapsulate the user details, assigned role and permissions.
- Create the JWT utility files
```go
// create a util directory
$ mkdir util
$ touch jwt.go
$ touch jwtAuth.go
```
- jwt.go
{% gist
https://gist.github.com/bensonmacharia/fb5e64d60ec147953e4c93171680ab2f %}
- jwtAuth.go
{% gist
https://gist.github.com/bensonmacharia/88add62fdc01477d8d59e71c4725a385 %}

- Add token to user login response
```go
// edit user controller and append 
func Login(context *gin.Context) {
     jwt, err := util.GenerateJWT(user)
     if err != nil {
		context.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	context.JSON(http.StatusOK, gin.H{"token": jwt, "username": input.Username, "message": "Successfully logged in"})
}
```

- Add admin functions routes
```go
// edit main.go
func serveApplication() {
        adminRoutes := router.Group("/admin")
	adminRoutes.Use(util.JWTAuth())
	adminRoutes.GET("/users", controller.GetUsers)
	adminRoutes.GET("/user/:id", controller.GetUser)
	adminRoutes.PUT("/user/:id", controller.UpdateUser)
	adminRoutes.POST("/user/role", controller.CreateRole)
	adminRoutes.GET("/user/roles", controller.GetRoles)
	adminRoutes.PUT("/user/role/:id", controller.UpdateRole)
}
```
**_Step 7: Run and Test Admin Funtions_**
- Run API
```go
$  go run main.go
```
- Admin login
```go
// admin user login
$ curl -X POST http://localhost:8000/auth/user/login \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -d '{"username": "test","password":"super^Secret!007"}' 
```
- Get All Users
```go
// use admin token from login response
$ curl -X GET http://localhost:8000/admin/users \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"username": "test","password":"super^Secret!007"}'
```
- Get User by ID
```go
$ curl -X GET http://localhost:8000/admin/user/1 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8"
```
- Update User
```go
$ curl -X PUT http://localhost:8000/admin/user/2 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"username": "test","email":"test@gmail.com","role_id":"2"}'
```
- Create Role
```go
$ curl -X POST http://localhost:8000/admin/user/role \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"name": "testing","description":"Test user role"}'
```
- Get All Roles
```go
$ curl -X GET http://localhost:8000/admin/user/roles \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8"
```
- Update Role
```go
$ curl -X PUT http://localhost:8000/admin/user/role/4 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"name":"accountant","description":"Accountant user role"}'
```
- Add Room
```go
$ curl -X POST http://localhost:8000/admin/room/add \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODQ0NDc1ODQsImlhdCI6MTY4NDQ0NTc4NCwiaWQiOjEsInJvbGUiOjF9.CKQf2GggCP1cnGqfTp_2R77Q7GsQBX_dxf5PSLEbTx8" \
     -d '{"name": "Room 9","location":"Second Floor"}'
```
- List all Rooms
```go
$ curl -X GET http://localhost:8000/api/view/rooms \
     -H "Content-Type: application/json" \
     -H "Accept: application/json"
```
- Get Room by ID
```go
$ curl -X GET http://localhost:8000/api/view/room/3 \
     -H "Content-Type: application/json" \
     -H "Accept: application/json"
```

**_Step 8: Test the room booking service_**
- Book a Room
```go
$ curl -X POST http://localhost:8000/api/room/book \
     -H "Content-Type: application/json" \
     -H "Accept: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODUyMjk0MDEsImlhdCI6MTY4NTIyNzYwMSwiaWQiOjI0LCJyb2xlIjoyfQ.h8R51DA5N_xeCa8xR1HLeOo4JTmIGjUp3oMPJLuBv3g" \
     -d '{"room_id": 3}'
```
- List all Bookings
```go
$ curl -X GET http://localhost:8000/admin/room/bookings \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODcyOTkyMTAsImlhdCI6MTY4NzI5NzQxMCwiaWQiOjEsInJvbGUiOjF9.3oztz8EgE-l3byKWzCI760FE-BmRY7B-BohnYydDElc" \
     -H "Accept: application/json"
```
- List all User Bookings
```go
$ curl -X GET http://localhost:8000/api/rooms/booked \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlYXQiOjE2ODUyMjk0MDEsImlhdCI6MTY4NTIyNzYwMSwiaWQiOjI0LCJyb2xlIjoyfQ.h8R51DA5N_xeCa8xR1HLeOo4JTmIGjUp3oMPJLuBv3g" \
     -H "Accept: application/json"
```
**Winding up**
RBAC presents the simplest form of access control that can help prevent unauthorised access to data. This facilitates compliance to various regulatory and compliance requirements especially those related to data protection, privacy and system access. With RBAC, it's also easy to maintain an audit trail of all user activities which can significantly help speed up incident response process.

**Let's connect**
[LinkedIn](https://www.linkedin.com/in/benson-macharia/)
[Twitter](https://twitter.com/bensonmachariah)
[Blog](https://bmacharia.com/)