Skip to main content

Echo JWT Authentication

Introduction

JWT (JSON Web Token) authentication is a popular method for securing web applications. It allows for stateless authentication, where the server doesn't need to store session information. Instead, all necessary user data is encoded in a token that the client presents with each request.

In this guide, we'll learn how to implement JWT authentication in the Echo web framework for Go. We'll cover what JWT is, why it's useful, and how to integrate it into your Echo applications with step-by-step examples.

What is JWT?

JWT (pronounced "jot") stands for JSON Web Token. It's an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed.

A JWT consists of three parts:

  1. Header - Contains the type of token and the signing algorithm
  2. Payload - Contains the claims (user data and metadata)
  3. Signature - Used to verify the sender of the JWT and ensure the message wasn't changed

These parts are encoded and separated by dots, resulting in a string like: xxxxx.yyyyy.zzzzz

Why Use JWT in Echo?

  • Stateless Authentication: No need to store session data on the server
  • Portable: The same token can be used across different services
  • Scalable: Works well in distributed systems and microservices
  • Secure: When properly implemented, provides strong security guarantees
  • Rich Information: Can contain user roles, permissions and other claims

Getting Started with JWT in Echo

Prerequisites

Before we begin, make sure you have:

  1. Go installed on your system
  2. Basic understanding of Echo framework
  3. A Go project with Echo already set up

Installing Required Packages

First, let's install the necessary packages:

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

Basic JWT Authentication Implementation

Let's implement a basic JWT authentication system:

go
package main

import (
"net/http"
"time"

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

// User represents the user model
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
}

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

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

// Check credentials from database (simplified for demo)
if username != "john" || password != "secret" {
return echo.ErrUnauthorized
}

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

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

// Generate encoded token
secretKey := []byte("your-secret-key")
encodedToken, err := token.SignedString(secretKey)
if err != nil {
return err
}

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

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()

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

// Login route
e.POST("/login", login)

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

// Configure JWT middleware
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("your-secret-key"),
}

r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)

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

How It Works

Let's break down how JWT authentication works in the example above:

  1. User Authentication: When a user provides correct credentials in the /login endpoint, we generate a JWT
  2. Token Creation: We create custom claims including user information and an expiration time
  3. Token Signing: We sign the token with a secret key using the HS256 algorithm
  4. Token Response: We send the token back to the client
  5. Protected Routes: We create a group of routes that require authentication
  6. JWT Middleware: We use Echo's JWT middleware to validate tokens on protected routes
  7. Claims Extraction: In protected routes, we extract user information from the token

Testing the JWT Authentication

You can test the JWT authentication using curl:

  1. Login to obtain a token:
bash
curl -X POST -d "username=john&password=secret" http://localhost:8080/login

Output:

json
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
  1. Access a protected route using the token:
bash
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." http://localhost:8080/restricted

Output:

Welcome John Doe!
  1. Try accessing without a valid token:
bash
curl http://localhost:8080/restricted

Output:

{"message":"missing or malformed jwt"}

Advanced JWT Configuration

Customizing JWT Validation

You can customize the JWT validation by configuring the middleware:

go
// Configure JWT middleware with custom settings
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("your-secret-key"),
SigningMethod: "HS256",
TokenLookup: "header:Authorization,cookie:token",
AuthScheme: "Bearer",
ContextKey: "user",
ErrorHandler: func(err error) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Please authenticate first")
},
}

Using Environment Variables for Secret Keys

In a production environment, you should never hardcode your secret keys:

go
import "os"

// Load secret from environment variable
secretKey := os.Getenv("JWT_SECRET")
if secretKey == "" {
// Provide a default or exit with an error
secretKey = "fallback-secret-key-for-development-only"
}

// Use secretKey in JWT configuration
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte(secretKey),
}

Implementing Token Refresh

For a better user experience, you can implement token refresh functionality:

go
func refreshToken(c echo.Context) error {
// Get the token from the request
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)

// Create new claims with extended expiry
newClaims := &jwtCustomClaims{
claims.UserID,
claims.Name,
claims.Admin,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}

// Create new token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, newClaims)

// Generate encoded token
encodedToken, err := token.SignedString([]byte("your-secret-key"))
if err != nil {
return err
}

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

// Add refresh endpoint to your routes
r.GET("/refresh", refreshToken)

Real-World Application Example

Let's see a more complete example that includes user registration, database integration, and proper error handling:

go
package main

import (
"database/sql"
"net/http"
"time"
"golang.org/x/crypto/bcrypt"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/golang-jwt/jwt/v5"
_ "github.com/mattn/go-sqlite3"
)

// User represents the user model
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Password string `json:"-"` // Don't return password in JSON responses
}

// LoginRequest represents login form data
type LoginRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}

// RegisterRequest represents registration form data
type RegisterRequest struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
}

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

// Global database connection
var db *sql.DB

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

// Create users table if not exists
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL
)
`)
if err != nil {
panic(err)
}
}

// Register a new user
func register(c echo.Context) error {
var req RegisterRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

if req.Username == "" || req.Password == "" {
return echo.NewHTTPError(http.StatusBadRequest, "Username and password required")
}

// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to hash password")
}

// Insert user into database
result, err := db.Exec("INSERT INTO users (username, password) VALUES (?, ?)", req.Username, hashedPassword)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Username may already exist")
}

id, _ := result.LastInsertId()

return c.JSON(http.StatusCreated, map[string]interface{}{
"id": id,
"username": req.Username,
"message": "User registered successfully",
})
}

// Login and get JWT token
func login(c echo.Context) error {
var req LoginRequest
if err := c.Bind(&req); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}

// Find user in database
var user User
var hashedPassword string
err := db.QueryRow("SELECT id, username, password FROM users WHERE username = ?", req.Username).Scan(&user.ID, &user.Username, &hashedPassword)
if err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}

// Compare password with hash
if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(req.Password)); err != nil {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}

// Set custom claims
claims := &JWTCustomClaims{
UserID: user.ID,
Name: user.Username,
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
secretKey := []byte("your-secure-secret-key") // In production, use environment variable
encodedToken, err := token.SignedString(secretKey)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate token")
}

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

// Profile endpoint returns user data from JWT token
func profile(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTCustomClaims)

var userInfo User
err := db.QueryRow("SELECT id, username FROM users WHERE id = ?", claims.UserID).Scan(&userInfo.ID, &userInfo.Username)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "User not found")
}

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

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

e := echo.New()

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

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

// JWT middleware
config := middleware.JWTConfig{
Claims: &JWTCustomClaims{},
SigningKey: []byte("your-secure-secret-key"),
}

// Protected group
r := e.Group("/api")
r.Use(middleware.JWTWithConfig(config))

// Protected routes
r.GET("/profile", profile)

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

Best Practices for JWT Authentication

  1. Always use HTTPS: JWT tokens sent over HTTP can be intercepted.

  2. Set reasonable expiration times: Short-lived tokens (e.g., 15-60 minutes) reduce the risk of token theft.

  3. Store tokens securely on the client: Use HTTP-only cookies or secure local storage.

  4. Implement refresh tokens: For better user experience without compromising security.

  5. Keep the payload small: JWT tokens should only contain essential information.

  6. Use environment variables for secrets: Never hardcode secrets in your application code.

  7. Validate all claims: Check expiration, issuer, audience, and other relevant claims.

  8. Implement token revocation: Consider using a token blacklist for scenarios like user logout.

Common Security Issues and Solutions

Token Theft

Issue: If a JWT is stolen, an attacker can use it until it expires.

Solution: Use short-lived tokens with refresh tokens, store tokens in HTTP-only cookies, and implement token blacklisting on logout.

Weak Secret Keys

Issue: Weak keys can be brute-forced to forge tokens.

Solution: Use strong cryptographically secure random keys that are at least 256 bits (32 bytes) long.

Not Validating Claims

Issue: Failing to validate claims allows expired or improperly issued tokens.

Solution: Always validate all claims, including expiration time, issuer, and audience.

Summary

In this guide, we've explored how to implement JWT authentication in Echo applications:

  1. We learned what JWT is and why it's useful for authentication
  2. We implemented basic JWT authentication with login and protected routes
  3. We explored advanced configuration options and best practices
  4. We built a complete real-world example with user registration and database integration
  5. We covered security best practices and common pitfalls to avoid

By following these steps, you can secure your Echo applications with stateless JWT authentication that scales well and provides a good balance between security and user experience.

Additional Resources

Exercises

  1. Implement token refresh functionality using refresh tokens
  2. Add role-based access control using JWT claims
  3. Create a JWT middleware that checks for specific permissions in claims
  4. Implement token revocation (blacklisting) for user logout
  5. Create a password reset system that uses short-lived JWTs


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