» Go: Build a REST API with Gin » 2. Development » 2.12 Authentication

Authentication

Authentication in an API server is typically needed when you want to control access to certain resources or functionalities.

In other words, if you want to restrict access to certain API endpoints or data to users with special roles (e.g., administrator, moderator, or premium user), authentication is required.

Traditional User Flow

Update code

Add User domain entity in domain/model/user.go:

package model

import "time"

// User represents an app user
type User struct {
	ID        uint      `json:"id,omitempty"`
	Email     string    `json:"email,omitempty"`
	Password  string    `json:"password,omitempty"`
	Salt      string    `json:"salt,omitempty"`
	IsAdmin   bool      `json:"is_admin,omitempty"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Declare its business capabilities in domain/gateway/user_manager.go:

package gateway

import (
	"context"

	"literank.com/rest-books/domain/model"
)

// UserManager manages users
type UserManager interface {
	CreateUser(ctx context.Context, u *model.User) (uint, error)
	GetUserByEmail(ctx context.Context, email string) (*model.User, error)
}

Add implementations in infrastructure/database/mysql.go:

@@ -23,7 +23,7 @@ func NewMySQLPersistence(dsn string, pageSize int) (*MySQLPersistence, error) {
                return nil, err
        }
        // Auto Migrate the data structs
-       db.AutoMigrate(&model.Book{})
+       db.AutoMigrate(&model.Book{}, &model.User{})
 
        return &MySQLPersistence{db, pageSize}, nil
 }
@@ -67,3 +67,18 @@ func (s *MySQLPersistence) GetBooks(ctx context.Context, offset int, keyword str
        }
        return books, nil
 }
+
+func (s *MySQLPersistence) CreateUser(ctx context.Context, u *model.User) (uint, error) {
+       if err := s.db.WithContext(ctx).Create(u).Error; err != nil {
+               return 0, err
+       }
+       return u.ID, nil
+}
+
+func (s *MySQLPersistence) GetUserByEmail(ctx context.Context, email string) (*model.User, error) {
+       var u model.User
+       if err := s.db.WithContext(ctx).Where("email = ?", email).First(&u).Error; err != nil {
+               return nil, err
+       }
+       return &u, nil
+}

Do the sign-up and sign-in preparation work in application/executor/user_operator.go:

package executor

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"errors"
	"math/rand"
	"time"

	"literank.com/rest-books/application/dto"
	"literank.com/rest-books/domain/gateway"
	"literank.com/rest-books/domain/model"
)

const (
	saltLen          = 4
	errEmptyEmail    = "empty email"
	errEmptyPassword = "empty password"
)

type UserOperator struct {
	userManager gateway.UserManager
}

func NewUserOperator(u gateway.UserManager) *UserOperator {
	return &UserOperator{userManager: u}
}

// CreateUser creates a new user
func (u *UserOperator) CreateUser(ctx context.Context, uc *dto.UserCredential) (*dto.User, error) {
	if uc.Email == "" {
		return nil, errors.New(errEmptyEmail)
	}
	if uc.Password == "" {
		return nil, errors.New(errEmptyPassword)
	}
	salt := randomString(saltLen)
	user := &model.User{
		Email:    uc.Email,
		Password: sha1Hash(uc.Password + salt),
		Salt:     salt,
	}
	uid, err := u.userManager.CreateUser(ctx, user)
	if err != nil {
		return nil, err
	}
	return &dto.User{
		ID:    uid,
		Email: uc.Email,
	}, nil
}

// SignIn signs an user in
func (u *UserOperator) SignIn(ctx context.Context, email, password string) (*dto.User, error) {
	if email == "" {
		return nil, errors.New(errEmptyEmail)
	}
	if password == "" {
		return nil, errors.New(errEmptyPassword)
	}
	user, err := u.userManager.GetUserByEmail(ctx, email)
	if err != nil {
		return nil, err
	}
	passwordHash := sha1Hash(password + user.Salt)
	if user.Password != passwordHash {
		return nil, errors.New("wrong password")
	}

	return &dto.User{
		ID:    user.ID,
		Email: user.Email,
	}, nil
}

func randomString(length int) string {
	source := rand.NewSource(time.Now().UnixNano())
	random := rand.New(source)
	const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
	result := make([]byte, length)
	for i := range result {
		result[i] = charset[random.Intn(len(charset))]
	}
	return string(result)
}

func sha1Hash(input string) string {
	h := sha1.New()
	h.Write([]byte(input))
	hashBytes := h.Sum(nil)
	hashString := hex.EncodeToString(hashBytes)
	return hashString
}

func randomString and sha1Hash are helper functions for salt generation and password hashing.

Why do you need a salt?

Without a salt, an attacker could precompute hashes for common passwords and store them in a table (known as a rainbow table). If the database is compromised, the attacker can then compare the hashed passwords against the precomputed hashes to quickly identify passwords. Adding a salt to each password ensures that even if two users have the same password, their hashed passwords will be different due to the unique salt.

Even if a user chooses a weak password, such as a common dictionary word, the salt ensures that the resulting hash is unique. Without a salt, all instances of the same password would hash to the same value, making them vulnerable to attacks.

Add DTOs in application/dto/user.go:

package dto

// UserCredential represents the user's sign-in email and password
type UserCredential struct {
	Email    string `json:"email,omitempty"`
	Password string `json:"password,omitempty"`
}

// User is used as result of a successful sign-in
type User struct {
	ID    uint   `json:"id,omitempty"`
	Email string `json:"email,omitempty"`
}

DTO stands for Data Transfer Object.

Often, the internal representation of data within your application might differ from what you want to expose through your API. DTOs allow you to transform your internal data structures into a format that's more suitable for the API consumers. This helps in decoupling the internal representation from the external one, allowing for better flexibility and abstraction.

Wire in new dependencies in application/wire_helper.go:

@@ -31,6 +31,10 @@ func (w *WireHelper) BookManager() gateway.BookManager {
        return w.sqlPersistence
 }
 
+func (w *WireHelper) UserManager() gateway.UserManager {
+       return w.sqlPersistence
+}
+
 func (w *WireHelper) ReviewManager() gateway.ReviewManager {
        return w.noSQLPersistence
 }

Finnally, add routes in adaptor/router.go:

@@ -22,12 +22,14 @@ const (
 type RestHandler struct {
        bookOperator   *executor.BookOperator
        reviewOperator *executor.ReviewOperator
+       userOperator   *executor.UserOperator
 }
 
 func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
        rest := &RestHandler{
                bookOperator:   executor.NewBookOperator(wireHelper.BookManager(), wireHelper.CacheHelper()),
                reviewOperator: executor.NewReviewOperator(wireHelper.ReviewManager()),
+               userOperator:   executor.NewUserOperator(wireHelper.UserManager()),
        }
        // Create a new Gin router
        r := gin.Default()
@@ -49,6 +51,10 @@ func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
        r.POST("/reviews", rest.createReview)
        r.PUT("/reviews/:id", rest.updateReview)
        r.DELETE("/reviews/:id", rest.deleteReview)
+
+       userGroup := r.Group("/users")
+       userGroup.POST("", rest.userSignUp)
+       userGroup.POST("/sign-in", rest.userSignIn)
        return r, nil
 }

@@ -220,3 +226,33 @@ func (r *RestHandler) deleteReview(c *gin.Context) {
        }
        c.JSON(http.StatusNoContent, nil)
 }
+
+func (r *RestHandler) userSignUp(c *gin.Context) {
+       var ucBody dto.UserCredential
+       if err := c.ShouldBindJSON(&ucBody); err != nil {
+               c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+               return
+       }
+
+       u, err := r.userOperator.CreateUser(c, &ucBody)
+       if err != nil {
+               fmt.Printf("Failed to create user: %v\n", err)
+               c.JSON(http.StatusNotFound, gin.H{"error": "failed to sign up"})
+               return
+       }
+       c.JSON(http.StatusCreated, u)
+}
+
+func (r *RestHandler) userSignIn(c *gin.Context) {
+       var m dto.UserCredential
+       if err := c.ShouldBindJSON(&m); err != nil {
+               c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+               return
+       }
+       u, err := r.userOperator.SignIn(c, m.Email, m.Password)
+       if err != nil {
+               c.JSON(http.StatusOK, gin.H{"error": err.Error()})
+               return
+       }
+       c.JSON(http.StatusOK, u)
+}

Try with curl

User sign up

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8080/users

Result:

{"id":2,"email":"test-user@example.com"}

If you check out the records in the database, you will find something like this:

+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+
| id | email                 | password                                 | salt | is_admin | created_at              | updated_at              |
+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+
|  2 | test-user@example.com | f9264ef99598a1be989cfca6a692cb677dbe7ac4 | 4Fnd |        0 | 2024-02-27 11:44:05.350 | 2024-02-27 11:44:05.350 |
+----+-----------------------+------------------------------------------+------+----------+-------------------------+-------------------------+

User sign in successfully

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8080/users/sign-in

Result:

{"id":2,"email":"test-user@example.com"}

User sign in with an incorrect password

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "wrong-pass"}' http://localhost:8080/users/sign-in

Result:

{"error":"wrong password"}

Note:
It's good practice to use a uniform JSON response format for both successful and failed responses.
e.g.

{"code":0, "message":"ok", "data": {"id":2,"email":"test-user@example.com"}}
{"code":1001, "message":"wrong password", "data": null}

Use JWT (JSON Web Token)

JWT, or JSON Web Token, is a compact, URL-safe means of representing claims to be transferred between two parties. It's often used for authentication and authorization in web applications and APIs.

Install JWT dependency

go get -u github.com/golang-jwt/jwt/v5

Update code

Add UserPermission enums in domain/model/user.go:

@@ -2,6 +2,17 @@ package model
 
 import "time"
 
+// UserPermission represents different levels of user permissions.
+type UserPermission uint8
+
+// UserPermission levels
+const (
+       PermNone UserPermission = iota
+       PermUser
+       PermAuthor
+       PermAdmin
+)
+
 // User represents an app user
 type User struct {
        ID        uint      `json:"id,omitempty"`

Declare its business capabilities in domain/gateway/user_manager.go:

@@ -11,3 +11,9 @@ type UserManager interface {
        CreateUser(ctx context.Context, u *model.User) (uint, error)
        GetUserByEmail(ctx context.Context, email string) (*model.User, error)
 }
+
+// PermissionManager manage user permissions by tokens
+type PermissionManager interface {
+       GenerateToken(userID uint, email string, perm model.UserPermission) (string, error)
+       HasPermission(tokenResult string, perm model.UserPermission) (bool, error)
+}

Implement these 2 methods in infrastructure/token/jwt.go:

package token

import (
	"errors"
	"time"

	"github.com/golang-jwt/jwt/v5"

	"literank.com/rest-books/domain/model"
)

const (
	errInvalidToken  = "invalid token"
	errFailToConvert = "failed to convert token type"
)

// Keeper manages user tokens.
type Keeper struct {
	secretKey   []byte
	expireHours uint
}

// UserClaims includes user info.
type UserClaims struct {
	UserID     uint                 `json:"user_id,omitempty"`
	UserName   string               `json:"user_name,omitempty"`
	Permission model.UserPermission `json:"permission,omitempty"`
	jwt.RegisteredClaims
}

// NewTokenKeeper constructs a new JWT token keeper
func NewTokenKeeper(secretKey string, expireInHours uint) *Keeper {
	return &Keeper{[]byte(secretKey), expireInHours}
}

// GenerateToken generates a new JWT token.
func (t *Keeper) GenerateToken(userID uint, email string, perm model.UserPermission) (string, error) {
	claims := UserClaims{
		userID, email, perm,
		jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(t.expireHours) * time.Hour)),
		},
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenResult, err := token.SignedString(t.secretKey)
	if err != nil {
		return "", err
	}
	return tokenResult, nil
}

// ExtractToken extracts the token from the signed string.
func (t *Keeper) ExtractToken(tokenResult string) (*UserClaims, error) {
	token, err := jwt.ParseWithClaims(tokenResult, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
		return t.secretKey, nil
	})
	if err != nil {
		return nil, err
	}
	if !token.Valid {
		return nil, errors.New(errInvalidToken)
	}
	claims, ok := token.Claims.(*UserClaims)
	if !ok {
		return nil, errors.New(errFailToConvert)
	}
	return claims, nil
}

// HasPermission checks if user has the given permission.
func (t *Keeper) HasPermission(tokenResult string, perm model.UserPermission) (bool, error) {
	claims, err := t.ExtractToken(tokenResult)
	if err != nil {
		return false, err
	}
	return claims.Permission >= perm, nil
}

Add config items for Tokens in infrastructure/config/config.go:

@@ -21,8 +21,10 @@ type DBConfig struct {
 }
 
 type ApplicationConfig struct {
-       Port     int `json:"port" yaml:"port"`
-       PageSize int `json:"page_size" yaml:"page_size"`
+       Port        int    `json:"port" yaml:"port"`
+       PageSize    int    `json:"page_size" yaml:"page_size"`
+       TokenSecret string `json:"token_secret" yaml:"token_secret"`
+       TokenHours  int    `json:"token_hours" yaml:"token_hours"`
 }
 
 type CacheConfig struct {

Put in setting values in config.yml:

@@ -1,6 +1,8 @@
 app:
   port: 8080
   page_size: 5
+  token_secret: "I_Love_LiteRank"
+  token_hours: 72
 db:
   file_name: "test.db"

Generate tokens and return them in application/executor/user_operator.go:

@@ -21,10 +21,11 @@ const (
 
 type UserOperator struct {
        userManager gateway.UserManager
+       permManager gateway.PermissionManager
 }
 
-func NewUserOperator(u gateway.UserManager) *UserOperator {
-       return &UserOperator{userManager: u}
+func NewUserOperator(u gateway.UserManager, p gateway.PermissionManager) *UserOperator {
+       return &UserOperator{userManager: u, permManager: p}
 }
 
 // CreateUser creates a new user
@@ -52,7 +53,7 @@ func (u *UserOperator) CreateUser(ctx context.Context, uc *dto.UserCredential) (
 }
 
 // SignIn signs an user in
-func (u *UserOperator) SignIn(ctx context.Context, email, password string) (*dto.User, error) {
+func (u *UserOperator) SignIn(ctx context.Context, email, password string) (*dto.UserToken, error) {
        if email == "" {
                return nil, errors.New(errEmptyEmail)
        }
@@ -67,13 +68,33 @@ func (u *UserOperator) SignIn(ctx context.Context, email, password string) (*dto
        if user.Password != passwordHash {
                return nil, errors.New("wrong password")
        }
+       token, err := u.permManager.GenerateToken(user.ID, user.Email, calcPerm(user.IsAdmin))
+       if err != nil {
+               return nil, err
+       }
 
-       return &dto.User{
-               ID:    user.ID,
-               Email: user.Email,
+       return &dto.UserToken{
+               User: dto.User{
+                       ID:    user.ID,
+                       Email: user.Email,
+               },
+               Token: token,
        }, nil
 }
 
+// HasPermission checks if user has the given permission.
+func (u *UserOperator) HasPermission(tokenResult string, perm model.UserPermission) (bool, error) {
+       return u.permManager.HasPermission(tokenResult, perm)
+}
+
+func calcPerm(isAdmin bool) model.UserPermission {
+       perm := model.PermUser
+       if isAdmin {
+               perm = model.PermAdmin
+       }
+       return perm
+}
+
 func randomString(length int) string {
        source := rand.NewSource(time.Now().UnixNano())
        random := rand.New(source)

Add DTOs in application/dto/user.go:

@@ -11,3 +11,9 @@ type User struct {
        ID    uint   `json:"id,omitempty"`
        Email string `json:"email,omitempty"`
 }
+
+// UserToken is a combination of the User struct and the token field
+type UserToken struct {
+       User  User   `json:"user,omitempty"`
+       Token string `json:"token,omitempty"`
+}

Tune application/wire_helper.go:

@@ -5,6 +5,7 @@ import (
        "literank.com/rest-books/infrastructure/cache"
        "literank.com/rest-books/infrastructure/config"
        "literank.com/rest-books/infrastructure/database"
+       "literank.com/rest-books/infrastructure/token"
 )
 
 // WireHelper is the helper for dependency injection
@@ -12,6 +13,7 @@ type WireHelper struct {
        sqlPersistence   *database.MySQLPersistence
        noSQLPersistence *database.MongoPersistence
        kvStore          *cache.RedisCache
+       tokenKeeper      *token.Keeper
 }
 
 func NewWireHelper(c *config.Config) (*WireHelper, error) {
@@ -24,7 +26,10 @@ func NewWireHelper(c *config.Config) (*WireHelper, error) {
                return nil, err
        }
        kv := cache.NewRedisCache(&c.Cache)
-       return &WireHelper{sqlPersistence: db, noSQLPersistence: mdb, kvStore: kv}, nil
+       tk := token.NewTokenKeeper(c.App.TokenSecret, uint(c.App.TokenHours))
+       return &WireHelper{
+               sqlPersistence: db, noSQLPersistence: mdb,
+               kvStore: kv, tokenKeeper: tk}, nil
 }
 
 func (w *WireHelper) BookManager() gateway.BookManager {
@@ -35,6 +40,10 @@ func (w *WireHelper) UserManager() gateway.UserManager {
        return w.sqlPersistence
 }
 
+func (w *WireHelper) PermManager() gateway.PermissionManager {
+       return w.tokenKeeper
+}
+
 func (w *WireHelper) ReviewManager() gateway.ReviewManager {
        return w.noSQLPersistence
 }

Add PermCheck middleware into routes for Create/Update/Delete operations in adaptor/router.go:

@@ -25,12 +25,16 @@ type RestHandler struct {
        userOperator   *executor.UserOperator
 }
 
-func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
-       rest := &RestHandler{
+func newRestHandler(wireHelper *application.WireHelper) *RestHandler {
+       return &RestHandler{
                bookOperator:   executor.NewBookOperator(wireHelper.BookManager(), wireHelper.CacheHelper()),
                reviewOperator: executor.NewReviewOperator(wireHelper.ReviewManager()),
-               userOperator:   executor.NewUserOperator(wireHelper.UserManager()),
+               userOperator:   executor.NewUserOperator(wireHelper.UserManager(), wireHelper.PermManager()),
        }
+}
+
+func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
+       rest := newRestHandler(wireHelper)
        // Create a new Gin router
        r := gin.Default()
 
@@ -43,9 +47,9 @@ func MakeRouter(wireHelper *application.WireHelper) (*gin.Engine, error) {
        })
        r.GET("/books", rest.getBooks)
        r.GET("/books/:id", rest.getBook)
-       r.POST("/books", rest.createBook)
-       r.PUT("/books/:id", rest.updateBook)
-       r.DELETE("/books/:id", rest.deleteBook)
+       r.POST("/books", rest.PermCheck(model.PermAuthor), rest.createBook)
+       r.PUT("/books/:id", rest.PermCheck(model.PermAuthor), rest.updateBook)
+       r.DELETE("/books/:id", rest.PermCheck(model.PermAuthor), rest.deleteBook)
        r.GET("/books/:id/reviews", rest.getReviewsOfBook)
        r.GET("/reviews/:id", rest.getReview)
        r.POST("/reviews", rest.createReview)

PermCheck is implemented in adaptor/middleware.go:

package adaptor

import (
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"

	"literank.com/rest-books/domain/model"
)

const tokenPrefix = "Bearer "

// PermCheck checks user permission
func (r *RestHandler) PermCheck(allowPerm model.UserPermission) gin.HandlerFunc {
	return func(c *gin.Context) {
		authHeader := c.Request.Header.Get("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{"error": "token is required"})
			c.Abort()
			return
		}
		token := strings.Replace(authHeader, tokenPrefix, "", 1)
		hasPerm, err := r.userOperator.HasPermission(token, allowPerm)
		message := "Unauthorized"
		if err != nil {
			message = err.Error()
		}
		if !hasPerm {
			c.JSON(http.StatusUnauthorized, gin.H{"error": message})
			c.Abort()
			return
		}
		c.Next()
	}
}

Those are all the changes you need to utilize JWT in your api server. Let's try it out again!

Try with curl

User sign in and get a Token

curl -X POST -H "Content-Type: application/json" -d '{"email": "test-user@example.com", "password": "test-pass"}' http://localhost:8080/users/sign-in

Result:

{
  "user": { "id": 2, "email": "test-user@example.com" },
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoxLCJleHAiOjE3MDkyNzA1MzV9.eJp5Yr9YZMxHFs5eId78bEJ6draO178jysquZ2VV9v8"
}

Put the token into Debugger on https://jwt.io/, then you can check the payload and verify the signature of your token.

JWT Debugger

Create a new book with a valid and proper Token

curl -X POST \
  http://localhost:8080/books \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjozLCJleHAiOjE3MDkyNzExMjl9.ybMnzuP0v__JVviWDKxeKaeTqpAn6wQL9CXfiNj7cM4' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Successful result:

{
  "id": 14,
  "title": "Test Book",
  "author": "John Doe",
  "published_at": "2003-01-01",
  "description": "A sample book description",
  "isbn": "1234567890",
  "total_pages": 100,
  "created_at": "2024-02-27T13:32:40.5+08:00",
  "updated_at": "2024-02-27T13:32:40.5+08:00"
}

Create a new book with a Token, but it doesn't have proper permissions

curl -X POST \
  http://localhost:8080/books \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoyLCJ1c2VyX25hbWUiOiJ0ZXN0LXVzZXJAZXhhbXBsZS5jb20iLCJwZXJtaXNzaW9uIjoxLCJleHAiOjE3MDkyNzA1MzV9.eJp5Yr9YZMxHFs5eId78bEJ6draO178jysquZ2VV9v8' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Result:

{"error":"Unauthorized"}

Create a new book without a Token

curl -X POST \
  http://localhost:8080/books \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Result:

{"error":"token is required"}

Create a new book with a fake Token

curl -X POST \
  http://localhost:8080/books \
  -H 'Authorization: Bearer FAKE_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Test Book",
    "author": "John Doe",
    "published_at": "2003-01-01",
    "description": "A sample book description",
    "isbn": "1234567890",
    "total_pages": 100
}'

Result:

{"error":"token is malformed: token contains an invalid number of segments"}

Perfect! 🎉

Now, some of your api endpoints are protected by the authentication mechanism.

PrevNext