Contents

Buffalo API with JWT Authentication

This guide is thinking to share my experience using Buffalo Framework for Golang and how to create API with JWT authentication in the simplest way.

Before starting

What will we use?

I assume that …

  • You have installed Golang, Buffalo, and Postgres.

Let’s Go

Creating Buffalo API

To create our a new Buffalo API using next command:

1
$ buffalo new api_name --api

If you want to use another type of database, you can use this flag --db-type "mysql" or --db-type "sqlite3" by default buffalo use postgres. View more

Once buffalo finishes creating the project, it will show us the following messages:

1
2
3
INFO[2020-03-07T13:31:42-06:00] Congratulations! Your application, auth-api, has been successfully built!
INFO[2020-03-07T13:31:42-06:00] You can find your new application at: /Users/saherla/Desktop/Little-Wire-Golang/auth_api
INFO[2020-03-07T13:31:42-06:00] Please read the README.md file in your new application for next steps on running your application.

The name of my project is auth_api. Therefore we access the dir:

1
2
3
$ cd auth_api
$ ls
Dockerfile       README.md        actions          config           database.yml     fixtures         go.mod           go.sum           grifts           inflections.json main.go          models

You can use any text editor, in my case I’m going to use Visual Studio Code.

The most important dir’s for me are actions and models.

/images/buffalo-auth/vs1.png

User Model

First, we generate our models using the buffalo commands:

  • The long command:
1
$ buffalo pop generate model user  
  • The short command:
1
$ buffalo pop g m user  

Note
Check your list of plugins you must have installed pop.
1
2
3
4
5
6
$ buffalo plugins list                                
| Bin         | Command               | Description                                    |
| ----------- | --------------------- | ---------------------------------------------- |
| buffalo-pop | buffalo db            | [DEPRECATED] please use `buffalo pop` instead. |
| buffalo-pop | buffalo destroy model | Destroys model files.                          |
| buffalo-pop | buffalo pop           | A tasty treat for all your database needs      |

When we create our models, buffalo automatically generates a migration dir, where our fizz files are stored to migrate our database to Postgres. One file is to upload our database and the other is to remove our database. View more

/images/buffalo-auth/vs2.png

We will add new fields to our model. In this case, add username, email and password.

1
2
3
4
5
6
7
8
9
// User is used by pop to map your .model.Name.Proper.Pluralize.Underscore database table to your go code.
type User struct {
	ID        uuid.UUID `json:"id" db:"id"`
	CreatedAt time.Time `json:"created_at" db:"created_at"`
	UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
	Username  string    `json:"username" db:"username"`
	Email     string    `json:"email" db:"email"`
	Password  string    `json:"password" db:"password"`
}

In the same way, we add these fields to our fizz file. Keep in mind that it must be to the file _create_user.up.fizz.

1
2
3
4
5
6
7
create_table("users") {
	t.Column("id", "uuid", {primary: true})
	t.Timestamps()
	t.Column("username", "string", {})
	t.Column("email", "string", {})
	t.Column("password", "string", {})
}

Validations User Model

Pop adds model-based validations, which is an excellent way to validate our API. View more

Validation Create

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// ValidateCreate gets run every time you call "pop.ValidateAndCreate" method.
// This method is not required and may be deleted.
func (u *User) ValidateCreate(tx *pop.Connection) (*validate.Errors, error) {
	var err error
	return validate.Validate(
		&validators.StringIsPresent{Field: u.Username, Name: "Username"},
		&validators.EmailIsPresent{Field: u.Email, Name: "Email", Message: "Incorrect Email Format"},
		&validators.StringIsPresent{Field: u.Password, Name: "Password"},
		&validators.StringLengthInRange{Field: u.Password, Name: "Password", Min: 5, Max: 50, Message: "The password must be greater than 6 characters"},
		// check to see if the email is already taken:
		&validators.FuncValidator{
			Field:   u.Email,
			Name:    "Email",
			Message: "%s has already been registered!",
			Fn: func() bool {
				var b bool
				q := tx.Where("email = ?", u.Email)
				if u.ID != uuid.Nil {
					q = q.Where("id != ?", u.ID)
				}
				b, err = q.Exists(u)
				if err != nil {
					return false
				}
				return !b
			},
		},
	), err
}

Validation Update

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ValidateUpdate gets run every time you call "pop.ValidateAndUpdate" method.
// This method is not required and may be deleted.
func (u *User) ValidateUpdate(tx *pop.Connection) (*validate.Errors, error) {
	var err error
	return validate.Validate(
		&validators.StringIsPresent{Field: u.Username, Name: "Username"},
		&validators.EmailIsPresent{Field: u.Email, Name: "Email", Message: "Incorrect Email Format"},
		// check to see if the email is already taken:
		&validators.FuncValidator{
			Field:   u.Email,
			Name:    "Email",
			Message: "%s has already been registered!",
			Fn: func() bool {
				var b bool
				q := tx.Where("email = ?", u.Email)
				if u.ID != uuid.Nil {
					q = q.Where("id != ?", u.ID)
				}
				b, err = q.Exists(u)
				if err != nil {
					return false
				}
				return !b
			},
		},
	), err
}

In this case, we will not be able to update the password in a future post we will perform an email verification.

Validations are quite important and interesting. You can see the validations available here.

Database Migration

You can configure your database in the database.yml file. I’ll leave the default setting.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
---
development:
  dialect: postgres
  database: auth_api_development
  user: postgres
  password: postgres
  host: 127.0.0.1
  pool: 5

test:
  url: {{envOr "TEST_DATABASE_URL" "postgres://postgres:[email protected]:5432/auth_api_test?sslmode=disable"}}

production:
  url: {{envOr "DATABASE_URL" "postgres://postgres:[email protected]:5432/auth_api_production?sslmode=disable"}}

First, we create the database using the buffalo commands.

1
2
3
4
5
6
7
8
9
$ buffalo db create -a 
v4.13.1

[POP] 2020/03/07 16:20:31 info - create auth_api_development (postgres://postgres:[email protected]:5432/auth_api_development?sslmode=disable)
[POP] 2020/03/07 16:20:31 info - created database auth_api_development
[POP] 2020/03/07 16:20:31 info - create auth_api_test (postgres://postgres:[email protected]:5432/auth_api_test?sslmode=disable)
[POP] 2020/03/07 16:20:31 info - created database auth_api_test
[POP] 2020/03/07 16:20:31 info - create auth_api_production (postgres://postgres:[email protected]:5432/auth_api_production?sslmode=disable)
[POP] 2020/03/07 16:20:31 info - created database auth_api_production

Then we migrate our tables from the _create_user.up.fizz file

1
2
3
4
5
6
$ buffalo db migrate up
v4.13.1

[POP] 2020/03/07 16:30:08 info - > create_users
[POP] 2020/03/07 16:30:08 info - 0.1008 seconds
[POP] 2020/03/07 16:30:08 warn - Migrator: unable to dump schema: exec: "pg_dump": executable file not found in $PATH

In our pgAdmin, we can visualize the database created.

/images/buffalo-auth/pg1.png

Callbacks

As the documentation says, “Pop provides a means to execute code before and after database operations.” View more

So we will create a callback to hash a user password.

1
2
3
4
5
6
7
8
9
// BeforeCreate callback to hash a user password.
func (u *User) BeforeCreate(tx *pop.Connection) error {
	hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
	if err != nil {
		return errors.WithStack(err)
	}
	u.Password = string(hash)
	return nil
}

User Actions

Already created our models and database, we generate with the buffalo commands our actions.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
$ buffalo g action --help
Generate new action(s)

Usage:
  buffalo generate action [name] [handler name...] [flags]

Aliases:
  action, a, actions

Flags:
  -d, --dry-run         dry run
  -h, --help            help for action
  -m, --method string   change the HTTP method for the generate action(s) (default "GET")
      --skip-template   skip generation of templates for action(s)
  -v, --verbose         verbosely run the generator

If you want more information about the generation of actions. Here

Create Users

  • The long command:
1
$ buffalo generate actions users create -m "POST" --skip-template
  • The short command:
1
$ buffalo g a users create -m "POST" --skip-template 

/images/buffalo-auth/vs3.png

When we generate our actions automatically it generates the routing in the app.go file.

1
app.POST("/users/create", UsersCreate)

I prefer that the methods (POST, GET, PATCH, PUT, DELETE…) define the action instead of indicating it on the route.

1
app.POST("/users", UsersCreate)

In our users.go file we add the following buffalo.Handler necessary to insert our users into the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// UsersCreate default implementation.
func UsersCreate(c buffalo.Context) error {
	// User Model
	user := &models.User{}

	// Bind user to the json elements.
	if err := c.Bind(user); err != nil {
		return errors.WithStack(err)
	}
	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}
	// Validate and create the data.
	verrs, err := tx.ValidateAndCreate(user)
	if err != nil {
		return errors.WithStack(err)
	}

	// verrs.HasAny returns true/false depending on whether any errors
	// have been tracked.
	if verrs.HasAny() {
		c.Set("errors", verrs)
		return c.Error(http.StatusConflict, errors.New(verrs.Error()))
	}

	return c.Render(http.StatusCreated, r.Auto(c, map[string]string{"message": "User Created"}))
}

We run the project to add a user.

1
$ buffalo dev
Note
I will use curl to create the queries you can use any REST Client (Insomnia, Postman). If you want to build curl commands you can use curlbuilder.
1
$ curl -XPOST -H "Content-type: application/json" -d '{"username": "test1","email": "[email protected]", "password": "test1"}' 'http://127.0.0.1:3000/users'

Output

1
{"message":"User Created"}

/images/buffalo-auth/pg2.png

Success
Perfect our API works correctly.

Read Users

  • The long command:
1
$ buffalo generate actions users read --skip-template
  • The short command:
1
$ buffalo g a users read --skip-template 

In our app.go file, we change the path. As we did in the method of Create Users.

1
app.GET("/users", UsersRead)

In users.go we add our buffalo.Handler necessary to read our users in the database.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// UsersRead default implementation.
func UsersRead(c buffalo.Context) error {
	users := &models.Users{}

	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}

	// Paginate results. Params "page" and "per_page" control pagination.
	// Default values are "page=1" and "per_page=20".
	// Add Order for date.
	q := tx.PaginateFromParams(c.Params()).Order("created_at asc")

	// Retrieve all Users from the DB. Select all except password.
	if err := q.Select(
		"id",
		"created_at",
		"updated_at",
		"username",
		"email",
	).All(users); err != nil {
		return errors.WithStack(err)
	}
	// Add the paginator to the context so it can be used in the template.
	c.Set("pagination", q.Paginator)

	return c.Render(http.StatusOK, r.Auto(c, users))
}

We run the project and read the users.

1
$ buffalo dev
1
$ curl -XGET -H "Content-type: application/json" 'http://127.0.0.1:3000/users'

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[{
    "id":"377d57e8-8317-40e1-be5a-90c9cdd02e2a",
    "created_at":"2020-03-07T17:51:10.05641Z",
    "updated_at":"2020-03-07T17:51:10.056424Z",
    "username":"test1",
    "email":"[email protected]",
    "password":""
    },
    {
    "id":"c5fc20f9-a43a-4d14-b858-c9fad45847cb",
    "created_at":"2020-03-07T18:25:59.940466Z",
    "updated_at":"2020-03-07T18:25:59.940484Z",
    "username":"test2",
    "email":"[email protected]",
    "password":""
}]

Read Users By ID

  • The long command:
1
$ buffalo generate actions users readByID --skip-template
  • The short command:
1
$ buffalo g a users readByID --skip-template

We change the path in our app.go file and add a parameter to extract user data depending on the ID.

1
app.GET("/users/{user_id}", UsersReadByID)

In our users.go file we add our buffalo.Handler function necessary to read a user by ID

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// UsersReadByID default implementation.
func UsersReadByID(c buffalo.Context) error {
	user := &models.User{}

	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}
	// Retrieve a User from the DB. Select all except password. Using parameter "user_id".
	if err := tx.Select(
		"id",
		"created_at",
		"updated_at",
		"username",
		"email",
	).Find(user, c.Param("user_id")); err != nil {
		return c.Error(http.StatusNotFound, err)
	}

	return c.Render(http.StatusOK, r.Auto(c, user))
}

We run the project and read the user by ID.

1
$ buffalo dev

We add the Hash ID in the path.

1
$ curl -XGET -H "Content-type: application/json" 'http://127.0.0.1:3000/users/377d57e8-8317-40e1-be5a-90c9cdd02e2a'

Output

1
2
3
4
5
6
7
8
{
    "id":"377d57e8-8317-40e1-be5a-90c9cdd02e2a",
    "created_at":"2020-03-07T17:51:10.05641Z",
    "updated_at":"2020-03-07T17:51:10.056424Z",
    "username":"test1",
    "email":"[email protected]",
    "password":""
}

Update Users

  • The long command:
1
$ buffalo generate actions users update -m "PATCH" --skip-template
  • The short command:
1
$ buffalo g a users update -m "PATCH"  --skip-template

Again, we change the path in our app.go file.

1
app.PATCH("/users/{user_id}", UsersUpdate)

We add in users.go the code of our buffalo.Handler function to update users.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// UsersUpdate default implementation.
func UsersUpdate(c buffalo.Context) error {
	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}
	// Allocate an empty User
	user := &models.User{}
	if err := tx.Find(user, c.Param("user_id")); err != nil {
		return c.Error(http.StatusNotFound, err)
	}

	// Bind Framework to the html form elements
	if err := c.Bind(user); err != nil {
		return errors.WithStack(err)
	}

	// Validate the data and exclude colum password.
	verrs, err := tx.ValidateAndUpdate(user, "password")
	if err != nil {
		return errors.WithStack(err)
	}

	// verrs.HasAny returns true/false depending on whether any errors
	// have been tracked.
	if verrs.HasAny() {
		c.Set("errors", verrs)
		return c.Error(http.StatusConflict, errors.New(verrs.Error()))
	}

	return c.Render(http.StatusCreated, r.Auto(c, map[string]string{"message": "User Updated"}))
}

We test our API by updating a registered user with an existing ID in our database.

1
$ buffalo dev
1
$ curl -XPATCH -H "Content-type: application/json" -d '{"username": "test1Update"}' 'http://127.0.0.1:3000/users/377d57e8-8317-40e1-be5a-90c9cdd02e2a'

Output

1
{"message":"User Updated"}

We check that our user has been updated.

1
$ curl -XGET -H "Content-type: application/json" 'http://127.0.0.1:3000/users/377d57e8-8317-40e1-be5a-90c9cdd02e2a'
1
2
3
4
5
6
7
8
{
    "id":"377d57e8-8317-40e1-be5a-90c9cdd02e2a",
    "created_at":"2020-03-07T17:51:10.05641Z",
    "updated_at":"2020-03-08T02:29:12.155629Z",
    "username":"test1Update",
    "email":"[email protected]",
    "password":""
}

Delete Users

  • The long command:
1
$ buffalo generate actions users delete -m "DELETE" --skip-template
  • The short command:
1
$ buffalo g a users delete -m "DELETE" --skip-template

We change the route to our last function in our app.go file.

1
app.DELETE("/users/{user_id}", UsersDelete)

We add our buffalo.Handler function to delete a user in users.go.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// UsersDelete default implementation.
func UsersDelete(c buffalo.Context) error {
	// Get the DB connection from the context
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}
	// Allocate an empty User
	user := &models.User{}

	// To find the Widget the parameter widget_id is used.
	if err := tx.Find(user, c.Param("user_id")); err != nil {
		return c.Error(404, err)
	}

	if err := tx.Destroy(user); err != nil {
		return errors.WithStack(err)
	}

	return c.Render(http.StatusCreated, r.Auto(c, map[string]string{"message": "User Deleted"}))
}

Now we will try to delete a user.

1
$ buffalo dev
1
$ curl -XDELETE -H "Content-type: application/json" 'http://127.0.0.1:3000/users/377d57e8-8317-40e1-be5a-90c9cdd02e2a'

Output

1
{"message":"User Deleted"}

We check if the user was deleted.

1
curl -XGET -H "Content-type: application/json" 'http://127.0.0.1:3000/users'

Output

1
2
3
4
5
6
7
8
[{
    "id":"c5fc20f9-a43a-4d14-b858-c9fad45847cb",
    "created_at":"2020-03-07T18:25:59.940466Z",
    "updated_at":"2020-03-07T18:25:59.940484Z",
    "username":"test2",
    "email":"[email protected]",
    "password":""
}]

Perfect we realize that the user test1Update has been deleted.

Authentication

Once our CRUD is complete and working, we will need to protect our routes by having a login that returns a token to access the routes that we protect.

When we create our buffalo API add a .env configuration file, this file is very important because in it we will add our secret key to our token. We add a new variable called JWT_SECRET in the .env configuration file. You can add the value you want, in my case I put “Buffalo”.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# This .env file was generated by buffalo, add here the env variables you need 
# buffalo to load into the ENV on application startup so your application works correctly.
# To add variables use KEY=VALUE format, you can later retrieve this in your application
# by using os.Getenv("KEY").
#
# Example:
# DATABASE_PASSWORD=XXXXXXXXX
# SESSION_SECRET=XXXXXXXXX
# SMTP_SERVER=XXXXXXXXX

JWT_SECRET="Buffalo"

Once added our secret key we generate a new action in my case I will call it auth.

1
$ buffalo g a auth login -m "POST" --skip-template   

In the same way, as with user actions, it automatically creates our path in app.go and our new file auth.go where the logic for user authentication will go.

We change the route in app.go

1
app.POST("/users/auth", AuthLogin)

Our function to authenticate the user in the auth.go file will have the following logic.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// AuthLogin default implementation.
func AuthLogin(c buffalo.Context) error {
	// User Model
	user := &models.User{}
	// Get the JWT Key Secret from .env file.
	secret := os.Getenv("JWT_SECRET")

	// Use Bind function to User model.
	if err := c.Bind(user); err != nil {
		return errors.WithStack(err)
	}

	// Save var of Request JSON Post.
	username := user.Username
	password := user.Password

	// We check if the username or password are not empty.
	if username == "" || password == "" {
		return c.Error(http.StatusBadRequest, errors.New("Username and password cannot be empty"))
	}

	// Get the DB connection from the context.
	tx, ok := c.Value("tx").(*pop.Connection)
	if !ok {
		return errors.WithStack(errors.New("no transaction found"))
	}

	// Find user with the username.
	q := tx.Select("id, username, password").Where("username= ?", username)
	err := q.First(user)
	if err != nil {
		if errors.Cause(err) == sql.ErrNoRows {
			// couldn't find an user with the supplied email.
			return c.Error(http.StatusUnauthorized, errors.New("Invalid username or password"))
		}
		return errors.WithStack(err)
	}

	// Get hashed password from db.
	PasswordHash := user.Password

	// Confirm that the given password matches the hashed password from the db
	err = bcrypt.CompareHashAndPassword([]byte(PasswordHash), []byte(password))
	if err != nil {
		return c.Error(http.StatusUnauthorized, errors.New("Invalid username or password"))
	}

	// Generate token with 2 hours expiration time.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
		"id":  user.ID,
		"exp": time.Now().Add(time.Hour * 2).Unix(),
	})

	tokenString, err := token.SignedString([]byte(secret))
	if err != nil {
		return errors.WithStack(err)
	}

	return c.Render(http.StatusAccepted, r.Auto(c, map[string]string{"token": tokenString}))
}

We test our login.

1
$ buffalo dev
1
$ curl -XPOST -H "Content-type: application/json" -d '{"username":"test2", "password":"test2"}' 'http://127.0.0.1:3000/users/auth'

Output

1
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM3MjA2ODcsImlkIjoiYzVmYzIwZjktYTQzYS00ZDE0LWI4NTgtYzlmYWQ0NTg0N2NiIn0.SARm7AFhi8n3WXnMqo5YD0o_dJW_TNUPyyZO8hFWNG0"}

Perfect, once with our token, we need to protect our routes and thus be able to use that token, for this we will use a package called mw-tokenauth.

In our app.go file, we will add the following:

1
2
// Save AuthMiddleware function.
AuthMiddleware := tokenauth.New(tokenauth.Options{})
1
2
// Adding to my api the function.
app.Use(AuthMiddleware)
1
2
// Disable Auth Middleware in these fuctions
app.Middleware.Skip(AuthMiddleware, AuthLogin, UsersCreate)

So our App function should be seen as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func App() *buffalo.App {
	if app == nil {
		app = buffalo.New(buffalo.Options{
			Env:          ENV,
			SessionStore: sessions.Null{},
			PreWares: []buffalo.PreWare{
				cors.Default().Handler,
			},
			SessionName: "_auth_api_session",
		})

		// Automatically redirect to SSL
		app.Use(forceSSL())

		// Log request parameters (filters apply).
		app.Use(paramlogger.ParameterLogger)

		// Set the request content type to JSON
		app.Use(contenttype.Set("application/json"))

		// Save AuthMiddleware function.
		AuthMiddleware := tokenauth.New(tokenauth.Options{})

		// Wraps each request in a transaction.
		//  c.Value("tx").(*pop.Connection)
		// Remove to disable this.
		app.Use(popmw.Transaction(models.DB))

		// Adding to my api the function.
		app.Use(AuthMiddleware)

		// Disable Auth Middleware in these fuctions
		app.Middleware.Skip(AuthMiddleware, AuthLogin, UsersCreate)

		app.GET("/", HomeHandler)
		app.POST("/users", UsersCreate)
		app.GET("/users", UsersRead)
		app.GET("/users/{user_id}", UsersReadByID)
		app.PATCH("/users/{user_id}", UsersUpdate)
		app.DELETE("/users/{user_id}", UsersDelete)

		app.POST("/users/auth", AuthLogin)
	}

	return app
}

We tested our API.

1
$ buffalo dev
1
$ curl -XPOST -H "Content-type: application/json" -d '{"username":"test2", "password":"test2"}' 'http://127.0.0.1:3000/users/auth'

Output

1
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM3MjIwNTcsImlkIjoiYzVmYzIwZjktYTQzYS00ZDE0LWI4NTgtYzlmYWQ0NTg0N2NiIn0.kSCFDKHpRlxqcBfoB8IK4UE335c2EyadSkev8whL9TE"}

If we want to read users we need to add our token to the headers as follows.

1
$ curl -XGET -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODM3MjIwNTcsImlkIjoiYzVmYzIwZjktYTQzYS00ZDE0LWI4NTgtYzlmYWQ0NTg0N2NiIn0.kSCFDKHpRlxqcBfoB8IK4UE335c2EyadSkev8whL9TE' -H "Content-type: application/json" 'http://127.0.0.1:3000/users'

As you can see the key of my header is Authorization and before placing the token I add Bearer, all this is found in the JWT documentation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[
	{
	"id":"c5fc20f9-a43a-4d14-b858-c9fad45847cb",
	"created_at":"2020-03-07T18:25:59.940466Z",
	"updated_at":"2020-03-07T18:25:59.940484Z",
	"username":"test2",
	"email":"[email protected]",
	"password":""
	}
]

As a result I get registered users, in this case I only have one.

Now you have the task to test the other protected routes and add as many as you want.

You can see the project in my repository in Gitlab.

If you liked the guide, share it and let me know, if you find any mistakes you can tell me we are here to learn from our mistakes and good luck.

You can also invite me for a coffee ☕️ Paypal

Thanks! ❤️