Skip to main content

Echo JWT Middleware

Introduction

JSON Web Tokens (JWT) have become a popular way to handle authentication in modern web applications. They provide a method to securely transmit information between parties as a JSON object. In this guide, we'll explore how to implement JWT authentication in your Echo applications using Echo's built-in JWT middleware.

JWT authentication allows you to secure your APIs and ensure that only authenticated users can access specific resources. This is particularly useful for creating RESTful APIs that need to maintain stateless authentication.

What is JWT?

Before diving into the middleware implementation, let's understand what JWT is:

JWT consists of three parts separated by dots:

  • Header: Contains the type of token and the signing algorithm
  • Payload: Contains the claims (data)
  • Signature: Used to verify the token hasn't been tampered with

A typical JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Installing Echo JWT Middleware

To use the JWT middleware in your Echo application, you first need to install it. Run the following command:

bash
go get github.com/labstack/echo-jwt/v4

Next, make sure you also have the JWT Go library installed:

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

Basic JWT Middleware Implementation

Let's start with a basic implementation of JWT middleware in an Echo application:

go
package main

import (
"net/http"
"time"

"github.com/labstack/echo/v4"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v4"
)

// jwtCustomClaims are custom claims extending default ones
type jwtCustomClaims struct {
Name string `json:"name"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}

func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")

// Validate username and password against database (simplified for this example)
if username != "john" || password != "password" {
return echo.ErrUnauthorized
}

// Set custom claims
claims := &jwtCustomClaims{
Name: "John Doe",
Admin: true,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}

// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Generate encoded token and send it as response
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}

return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}

func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
name := claims.Name
return c.String(http.StatusOK, "Welcome "+name+"!")
}

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

// Public routes
e.POST("/login", login)

// Restricted group
r := e.Group("/restricted")

// Configure middleware with the custom claims type
config := echojwt.Config{
NewClaimsFunc: func() jwt.Claims {
return new(jwtCustomClaims)
},
SigningKey: []byte("secret"),
}

r.Use(echojwt.WithConfig(config))
r.GET("", restricted)

e.Logger.Fatal(e.Start(":1323"))
}

Let's break down this code:

  1. We define custom claims by extending JWT's standard claims
  2. We create a login handler to generate tokens for authenticated users
  3. We set up a restricted route that requires a valid JWT
  4. We configure the JWT middleware with our custom claims and signing key

Testing the JWT Implementation

You can test this implementation using curl:

  1. First, get a token by logging in:
bash
curl -X POST -d "username=john" -d "password=password" http://localhost:1323/login

The response will contain a JWT token:

json
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
  1. Use this token to access the restricted endpoint:
bash
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:1323/restricted

If successful, you should see:

Welcome John Doe!

Customizing JWT Middleware

Echo's JWT middleware offers several configuration options to customize its behavior:

go
config := echojwt.Config{
// Function to retrieve claims from the token
NewClaimsFunc: func() jwt.Claims {
return new(jwtCustomClaims)
},
// Secret key for signing
SigningKey: []byte("secret"),
// Function to extract token from request
TokenLookup: "header:Authorization,cookie:token",
// Token prefix in Authorization header
AuthScheme: "Bearer",
// Context key to store user information
ContextKey: "user",
// Function to handle errors
ErrorHandler: func(c echo.Context, err error) error {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Unauthorized access",
})
},
}

Real-World Example: User Authentication System

Let's create a more complete example of a user authentication system using JWT:

go
package main

import (
"database/sql"
"net/http"
"time"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v4"
_ "github.com/mattn/go-sqlite3"
"golang.org/x/crypto/bcrypt"
)

// User represents a user in our system
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"` // Don't expose password in JSON
Email string `json:"email"`
Role string `json:"role"`
}

// JWTCustomClaims are custom claims extending default ones
type JWTCustomClaims struct {
UserID int `json:"user_id"`
Name string `json:"name"`
Role string `json:"role"`
jwt.RegisteredClaims
}

var db *sql.DB

func initDB() {
var err error
db, err = sql.Open("sqlite3", "./users.db")
if err != nil {
panic(err)
}

// Create users table if it doesn't exist
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
role TEXT NOT NULL DEFAULT 'user'
)
`)
if err != nil {
panic(err)
}
}

func register(c echo.Context) error {
u := new(User)
if err := c.Bind(u); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid data"})
}

// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to hash password"})
}

// Save to database
result, err := db.Exec(
"INSERT INTO users (username, password, email, role) VALUES (?, ?, ?, ?)",
u.Username, string(hashedPassword), u.Email, "user",
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create user"})
}

id, _ := result.LastInsertId()
u.ID = int(id)
u.Password = "" // Clear password before returning

return c.JSON(http.StatusCreated, u)
}

func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")

// Find user in database
var user User
err := db.QueryRow(
"SELECT id, username, password, email, role FROM users WHERE username = ?",
username,
).Scan(&user.ID, &user.Username, &user.Password, &user.Email, &user.Role)

if err != nil {
if err == sql.ErrNoRows {
return echo.ErrUnauthorized
}
return err
}

// Validate password
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
return echo.ErrUnauthorized
}

// Set custom claims
claims := &JWTCustomClaims{
UserID: user.ID,
Name: user.Username,
Role: user.Role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}

// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Generate encoded token and send it as response
t, err := token.SignedString([]byte("your-secret-key"))
if err != nil {
return err
}

return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}

func profile(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTCustomClaims)

// Get user from database
var u User
err := db.QueryRow(
"SELECT id, username, email, role FROM users WHERE id = ?",
claims.UserID,
).Scan(&u.ID, &u.Username, &u.Email, &u.Role)

if err != nil {
return echo.ErrNotFound
}

return c.JSON(http.StatusOK, u)
}

func adminOnly(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTCustomClaims)

if claims.Role != "admin" {
return c.JSON(http.StatusForbidden, map[string]string{
"error": "Admin access required",
})
}

return c.String(http.StatusOK, "Welcome to the admin area!")
}

func main() {
// Initialize database
initDB()
defer db.Close()

e := echo.New()

// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())

// Public routes
e.POST("/register", register)
e.POST("/login", login)

// JWT middleware configuration
config := echojwt.Config{
NewClaimsFunc: func() jwt.Claims {
return new(JWTCustomClaims)
},
SigningKey: []byte("your-secret-key"),
}

// Restricted routes
r := e.Group("/api")
r.Use(echojwt.WithConfig(config))
r.GET("/profile", profile)
r.GET("/admin", adminOnly)

e.Logger.Fatal(e.Start(":1323"))
}

This example demonstrates a complete authentication system with:

  1. User registration with password hashing
  2. Login with JWT generation
  3. Profile endpoint that requires authentication
  4. Admin-only section that checks user role

Best Practices for JWT Authentication

When implementing JWT authentication, consider these best practices:

  1. Store Tokens Securely: On the client side, store JWTs in HttpOnly cookies or secure storage, not in localStorage.

  2. Use HTTPS: Always use HTTPS in production to prevent token interception.

  3. Include Expiration: Add an expiration time to tokens to limit their validity period.

  4. Implement Token Refresh: Create a refresh token system to get new access tokens without requiring login.

  5. Keep Payloads Small: Don't store sensitive or large data in tokens; they're included in every request.

  6. Use Strong Secret Keys: Choose a strong, unique secret key for signing tokens.

  7. Implement Proper Error Handling: Return appropriate error messages when token validation fails.

Here's a simple implementation of token refresh:

go
func refreshToken(c echo.Context) error {
refreshToken := c.FormValue("refresh_token")

// Validate refresh token (implementation depends on your system)
// ...

// Generate new access token
claims := &JWTCustomClaims{
UserID: userId, // Retrieved from refresh token validation
Name: username,
Role: role,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
},
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString([]byte("your-secret-key"))
if err != nil {
return err
}

return c.JSON(http.StatusOK, map[string]string{
"token": t,
})
}

Common JWT Middleware Errors and Solutions

When working with JWT middleware, you might encounter these common issues:

  1. "token is expired"

    • Solution: Implement token refresh or redirect to login
  2. "signature is invalid"

    • Solution: Ensure the signing key matches between token generation and validation
  3. "token contains an invalid number of segments"

    • Solution: Check that the token format is valid and properly sent in the request
  4. Authorization header missing

    • Solution: Ensure the client is sending the token correctly in the Authorization header

Example of custom error handling:

go
config := echojwt.Config{
// ...other config options
ErrorHandler: func(c echo.Context, err error) error {
if err.Error() == "token is expired" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Your session has expired, please login again",
})
}
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid or missing authentication token",
})
},
}

Summary

In this guide, we've explored Echo's JWT middleware for implementing authentication in your Go web applications. We've learned:

  • How JWT authentication works
  • How to set up basic JWT middleware in Echo
  • How to create custom claims
  • How to build a complete user authentication system
  • Best practices for JWT authentication
  • Common errors and their solutions

JWT authentication provides a secure, stateless method for authenticating users in your API. Combined with Echo's powerful middleware system, it allows you to create robust authentication flows with minimal code.

Additional Resources

Exercises

  1. Implement a JWT authentication system with both access tokens and refresh tokens.
  2. Create a middleware that checks for specific permissions in the JWT claims.
  3. Modify the authentication system to use environment variables for the secret key.
  4. Implement token blacklisting for logged-out users.
  5. Add rate limiting to the login endpoint to prevent brute force attacks.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)