Skip to main content

Echo Token Management

In modern web applications, token-based authentication has become the standard approach for securing APIs and managing user sessions. This guide will walk you through the fundamentals of token management within the Echo framework, providing you with the knowledge to implement secure authentication in your applications.

Introduction to Token Management

Token-based authentication is a stateless authentication mechanism that allows users to verify their identity using a signed token. After successful authentication, the server generates a token (typically a JSON Web Token or JWT) that is sent back to the client. The client then includes this token in subsequent requests to access protected resources.

Echo provides robust support for token management, making it straightforward to implement authentication in your web applications.

Understanding JWT (JSON Web Tokens)

Before diving into Echo-specific implementations, let's understand what JWTs are:

A JWT consists of three parts separated by dots:

  • Header: Contains the token type and signing algorithm
  • Payload: Contains claims (user data and metadata)
  • Signature: Ensures the token hasn't been tampered with

A typical JWT looks like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Setting Up JWT Authentication in Echo

Let's start by implementing JWT authentication in an Echo application:

Step 1: Install Required Packages

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

Step 2: Create JWT Middleware

go
package main

import (
"net/http"
"time"

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

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

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 middleware with the custom claims type
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("secret"),
}
r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)

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

Step 3: Implement Login Handler

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

// Check credentials
if username != "jon" || password != "shhh!" {
return echo.ErrUnauthorized
}

// Set custom claims
claims := &jwtCustomClaims{
"Jon Snow",
true,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
},
}

// 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,
})
}

Step 4: Implement Restricted Handler

go
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+"!")
}

Token Creation and Generation

When generating tokens, consider the following best practices:

  1. Include minimal data in tokens: Only include necessary user information in the token payload.
  2. Set appropriate expiration times: Use short-lived tokens for better security.
  3. Store tokens securely: Clients should store tokens in secure HTTP-only cookies or secure local storage.

Example: Creating a Token with Specific Claims

go
func generateToken(userID string, isAdmin bool) (string, error) {
// Create token
token := jwt.New(jwt.SigningMethodHS256)

// Set claims
claims := token.Claims.(jwt.MapClaims)
claims["user_id"] = userID
claims["admin"] = isAdmin
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()

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

return tokenString, nil
}

Token Validation and Middleware

Echo makes token validation simple with its middleware. Here's how to customize token validation:

go
// Custom JWT middleware
func customJWTMiddleware() echo.MiddlewareFunc {
return middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("your-secret-key"),
SigningMethod: "HS256",
TokenLookup: "header:Authorization",
AuthScheme: "Bearer",
Claims: jwt.MapClaims{},
ErrorHandler: func(err error) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Please provide valid credentials")
},
})
}

Token Refresh Strategy

To enhance security while maintaining a good user experience, implement a token refresh strategy:

go
func refreshToken(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)

// Check if token is about to expire
exp := int64(claims["exp"].(float64))
if time.Unix(exp, 0).Sub(time.Now()) > 15*time.Minute {
return c.JSON(http.StatusBadRequest, echo.Map{
"message": "Token is not expired yet",
})
}

// Generate new token
userID := claims["user_id"].(string)
isAdmin := claims["admin"].(bool)

newToken, err := generateToken(userID, isAdmin)
if err != nil {
return err
}

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

Real-world Application: Complete Authentication Flow

Let's put everything together in a complete authentication flow:

go
package main

import (
"net/http"
"time"

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

type User struct {
ID string `json:"id"`
Username string `json:"username"`
Password string `json:"password"`
IsAdmin bool `json:"is_admin"`
}

// Mock user database
var users = map[string]User{
"user1": {
ID: "1",
Username: "john",
Password: "password123",
IsAdmin: false,
},
"user2": {
ID: "2",
Username: "admin",
Password: "admin123",
IsAdmin: true,
},
}

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

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

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

// Restricted routes
r := e.Group("/api")
r.Use(middleware.JWTWithConfig(middleware.JWTConfig{
SigningKey: []byte("secret-key"),
}))

r.GET("/profile", getProfile)
r.POST("/refresh", refreshToken)

// Admin only routes
admin := r.Group("/admin")
admin.Use(checkAdmin)
admin.GET("/stats", getStats)

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

func login(c echo.Context) error {
var credentials struct {
Username string `json:"username"`
Password string `json:"password"`
}

if err := c.Bind(&credentials); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid credentials format")
}

// Find user
var user User
var found bool

for _, u := range users {
if u.Username == credentials.Username && u.Password == credentials.Password {
user = u
found = true
break
}
}

if !found {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}

// Create token
token := jwt.New(jwt.SigningMethodHS256)

claims := token.Claims.(jwt.MapClaims)
claims["user_id"] = user.ID
claims["username"] = user.Username
claims["admin"] = user.IsAdmin
claims["exp"] = time.Now().Add(time.Hour * 24).Unix()

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

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

func register(c echo.Context) error {
var newUser User
if err := c.Bind(&newUser); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Invalid user data")
}

// Generate user ID
newUser.ID = fmt.Sprintf("%d", len(users)+1)

// Store user (in a real app, you'd use a database)
users[newUser.ID] = newUser

return c.JSON(http.StatusCreated, echo.Map{
"message": "User registered successfully",
"user_id": newUser.ID,
})
}

func getProfile(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
userID := claims["user_id"].(string)

if u, exists := users[userID]; exists {
// Don't return password
return c.JSON(http.StatusOK, echo.Map{
"id": u.ID,
"username": u.Username,
"is_admin": u.IsAdmin,
})
}

return echo.NewHTTPError(http.StatusNotFound, "User not found")
}

func checkAdmin(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
isAdmin := claims["admin"].(bool)

if !isAdmin {
return echo.NewHTTPError(http.StatusForbidden, "Admin access required")
}

return next(c)
}
}

func getStats(c echo.Context) error {
// Example admin stats
return c.JSON(http.StatusOK, echo.Map{
"total_users": len(users),
"active_users": len(users) - 2,
})
}

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

// Create new token
newToken := jwt.New(jwt.SigningMethodHS256)
newClaims := newToken.Claims.(jwt.MapClaims)

// Copy existing claims
for key, val := range claims {
newClaims[key] = val
}

// Update expiration time
newClaims["exp"] = time.Now().Add(time.Hour * 24).Unix()

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

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

Security Best Practices

When implementing token-based authentication, follow these security best practices:

  1. Use strong signing keys: Generate cryptographically secure keys for signing tokens.
  2. Implement token expiration: Always include expiration times in your tokens.
  3. Protect against XSS: Store tokens in HTTP-only cookies when possible.
  4. Implement CSRF protection: Use anti-CSRF tokens alongside your JWTs.
  5. Consider token revocation: Implement a strategy for revoking tokens when needed.
  6. Keep secrets safe: Never hardcode secrets in your source code (use environment variables).

Example: Loading JWT Secret from Environment

go
import (
"os"
// other imports
)

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

// Get JWT secret from environment variable
jwtSecret := os.Getenv("JWT_SECRET")
if jwtSecret == "" {
e.Logger.Fatal("JWT_SECRET environment variable is not set")
}

// Configure JWT middleware with the secret
config := middleware.JWTConfig{
SigningKey: []byte(jwtSecret),
}
// rest of your code
}

Token Revocation and Blacklisting

For enhanced security, implement token blacklisting to revoke tokens before their expiration:

go
// Simple in-memory token blacklist
var blacklistedTokens = make(map[string]time.Time)

// Middleware to check if token is blacklisted
func checkBlacklist(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Get("user").(*jwt.Token)
tokenString := c.Request().Header.Get("Authorization")[7:] // Remove "Bearer " prefix

if _, blacklisted := blacklistedTokens[tokenString]; blacklisted {
return echo.NewHTTPError(http.StatusUnauthorized, "Token has been revoked")
}

return next(c)
}
}

// Endpoint to logout (revoke token)
func logout(c echo.Context) error {
tokenString := c.Request().Header.Get("Authorization")[7:] // Remove "Bearer " prefix

// Get expiration time from token to know how long to keep it in blacklist
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(jwt.MapClaims)
exp := time.Unix(int64(claims["exp"].(float64)), 0)

// Add to blacklist
blacklistedTokens[tokenString] = exp

// Schedule cleanup of expired blacklisted tokens
go cleanupBlacklist()

return c.JSON(http.StatusOK, echo.Map{
"message": "Logged out successfully",
})
}

// Clean up expired tokens from blacklist
func cleanupBlacklist() {
for token, expiry := range blacklistedTokens {
if time.Now().After(expiry) {
delete(blacklistedTokens, token)
}
}
}

Summary

In this comprehensive guide, you've learned:

  1. How to implement JWT-based authentication in Echo
  2. Token creation and validation strategies
  3. How to implement token refresh mechanisms
  4. Security best practices for token management
  5. Real-world authentication flows
  6. Token revocation techniques

Effective token management is crucial for secure web applications. By following the principles outlined in this guide, you can create robust authentication systems that protect your users and data.

Additional Resources

Exercises

  1. Implement a complete authentication system with login, registration, and token refresh.
  2. Add role-based access control using JWT claims.
  3. Create a token blacklist system using Redis instead of in-memory storage.
  4. Implement multi-factor authentication alongside JWT authentication.
  5. Build a system that uses both access tokens and refresh tokens for enhanced security.


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