Skip to main content

Echo Authentication Middleware

Authentication is a critical aspect of web application security. It ensures that only legitimate users have access to protected resources. In the Echo framework, we can implement authentication through middleware - special functions that process requests before they reach route handlers. This tutorial will walk you through implementing and using authentication middleware in Echo.

Introduction to Echo Authentication Middleware

Authentication middleware in Echo intercepts incoming HTTP requests, validates the user's identity, and either allows the request to proceed to the handler or rejects it with an appropriate error response. This middleware sits between your client and your route handlers, acting as a gatekeeper for your protected resources.

Authentication Middleware Flow

Basic Authentication Middleware

Let's start with a simple username/password authentication middleware using Echo's built-in BasicAuth middleware.

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)

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

// Define credentials
credentials := make(map[string]string)
credentials["john"] = "secret123"
credentials["jane"] = "password456"

// BasicAuth middleware
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
// Check if username exists and password matches
if storedPassword, ok := credentials[username]; ok && storedPassword == password {
// Store authenticated user in context
c.Set("user", username)
return true, nil
}
return false, nil
}))

// Protected route
e.GET("/protected", func(c echo.Context) error {
user := c.Get("user").(string)
return c.String(http.StatusOK, "Welcome "+user+"!")
})

e.Start(":8080")
}

When you access /protected, the browser will prompt you for credentials. If you enter "john" as username and "secret123" as password, you'll see:

Welcome john!

If your credentials are incorrect, you'll receive a 401 Unauthorized response.

JWT Authentication Middleware

Basic authentication is simple but not suitable for modern web applications or APIs. JWT (JSON Web Token) authentication is more secure and scalable.

First, install the JWT middleware package:

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

Now, let's implement JWT authentication:

go
package main

import (
"net/http"
"time"

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

// User represents the user information
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 {
Username string `json:"username"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}

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

// Secret key for signing JWT tokens
secretKey := []byte("your-secret-key")

// Login route to generate JWT token
e.POST("/login", func(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")

// In a real application, verify against database
if username != "john" || password != "secret123" {
return echo.ErrUnauthorized
}

// Set claims
claims := &jwtCustomClaims{
Username: username,
Admin: username == "admin",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
},
}

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

// Generate encoded token
t, err := token.SignedString(secretKey)
if err != nil {
return err
}

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

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

// Configure JWT middleware
config := echojwt.Config{
SigningKey: secretKey,
SigningMethod: "HS256",
}

// Apply JWT middleware to restricted group
r.Use(echojwt.WithConfig(config))

// Protected route
r.GET("", func(c echo.Context) error {
// Get user from token
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(*jwtCustomClaims)

return c.String(http.StatusOK, "Welcome "+claims.Username+"!")
})

e.Start(":8080")
}

To test this JWT authentication:

  1. First, obtain a token by making a POST request to /login:
bash
curl -X POST -d "username=john&password=secret123" http://localhost:8080/login

You'll receive a response like:

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

If the token is valid, you'll see:

Welcome john!

Custom Authentication Middleware

Sometimes you may need to implement a custom authentication scheme. Here's an example of a custom API key middleware:

go
package main

import (
"net/http"

"github.com/labstack/echo/v4"
)

// APIKeyAuth middleware
func APIKeyAuth(apiKeys map[string]string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get API key from header
key := c.Request().Header.Get("X-API-Key")

// Check if API key exists
if key == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing API key")
}

// Validate API key
if username, valid := apiKeys[key]; valid {
// Store user in context
c.Set("user", username)
return next(c)
}

return echo.NewHTTPError(http.StatusUnauthorized, "Invalid API key")
}
}
}

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

// Define API keys (key -> username)
apiKeys := map[string]string{
"abc123": "john",
"def456": "jane",
}

// API key auth middleware for specific group
api := e.Group("/api")
api.Use(APIKeyAuth(apiKeys))

// Protected API route
api.GET("/data", func(c echo.Context) error {
user := c.Get("user").(string)
return c.JSON(http.StatusOK, map[string]string{
"message": "Hello, " + user + "!",
"data": "This is protected data",
})
})

e.Start(":8080")
}

To test this API key authentication:

bash
curl -H "X-API-Key: abc123" http://localhost:8080/api/data

You should receive:

json
{"message":"Hello, john!","data":"This is protected data"}

If you use an invalid API key or no key at all, you'll get a 401 Unauthorized error.

Role-Based Access Control

Let's extend our JWT example to implement role-based access control:

go
package main

import (
"net/http"
"time"

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

type jwtCustomClaims struct {
Username string `json:"username"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}

// RequireRole middleware checks if user has required role
func RequireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(*jwtCustomClaims)

// Check if user has the required role
hasRole := false
for _, r := range claims.Roles {
if r == role {
hasRole = true
break
}
}

if !hasRole {
return echo.ErrForbidden
}

return next(c)
}
}
}

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

secretKey := []byte("your-secret-key")

// Login route with roles
e.POST("/login", func(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")

// User database simulation
users := map[string]struct {
password string
roles []string
}{
"john": {"secret123", []string{"user"}},
"admin": {"admin123", []string{"user", "admin"}},
}

// Check credentials
user, exists := users[username]
if !exists || user.password != password {
return echo.ErrUnauthorized
}

// Set claims with roles
claims := &jwtCustomClaims{
Username: username,
Roles: user.roles,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 72)),
},
}

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

// Generate encoded token
t, err := token.SignedString(secretKey)
if err != nil {
return err
}

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

// JWT middleware config
config := echojwt.Config{
SigningKey: secretKey,
SigningMethod: "HS256",
NewClaimsFunc: func(c echo.Context) jwt.Claims {
return new(jwtCustomClaims)
},
}

// User routes - require 'user' role
userRoutes := e.Group("/user")
userRoutes.Use(echojwt.WithConfig(config))
userRoutes.Use(RequireRole("user"))

userRoutes.GET("/profile", func(c echo.Context) error {
token := c.Get("user").(*jwt.Token)
claims := token.Claims.(*jwtCustomClaims)

return c.String(http.StatusOK, "User profile for "+claims.Username)
})

// Admin routes - require 'admin' role
adminRoutes := e.Group("/admin")
adminRoutes.Use(echojwt.WithConfig(config))
adminRoutes.Use(RequireRole("admin"))

adminRoutes.GET("/dashboard", func(c echo.Context) error {
return c.String(http.StatusOK, "Admin Dashboard - Restricted Area")
})

e.Start(":8080")
}

To test this role-based authentication:

  1. Login as a regular user:
bash
curl -X POST -d "username=john&password=secret123" http://localhost:8080/login
  1. Access user profile with the token (should work):
bash
curl -H "Authorization: Bearer TOKEN_HERE" http://localhost:8080/user/profile
  1. Try to access admin dashboard with the same token (should fail):
bash
curl -H "Authorization: Bearer TOKEN_HERE" http://localhost:8080/admin/dashboard
  1. Login as admin:
bash
curl -X POST -d "username=admin&password=admin123" http://localhost:8080/login
  1. Use admin token to access admin dashboard (should work):
bash
curl -H "Authorization: Bearer ADMIN_TOKEN_HERE" http://localhost:8080/admin/dashboard

Best Practices for Authentication Middleware

  1. Don't store sensitive data in JWT tokens: Tokens can be decoded (although not verified without the secret key).

  2. Set appropriate token expiration: Short-lived tokens are more secure.

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

  4. Store secrets securely: Don't hardcode JWT secret keys in your code.

  5. Implement token refresh: Allow users to obtain a new token without re-authenticating.

  6. Add rate limiting: Prevent brute-force attacks by limiting authentication attempts.

Here's an example of implementing token refresh:

go
// Token refresh route
e.POST("/refresh", func(c echo.Context) error {
// Get refresh token from request
refreshToken := c.FormValue("refresh_token")

// Verify refresh token (in a real app, check against stored refresh tokens)
// ...

// Generate new access token
// ...

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

Summary

Authentication middleware in Echo provides a powerful way to secure your web applications and APIs. In this tutorial, we've covered:

  • Basic authentication middleware
  • JWT authentication middleware
  • Custom API key authentication
  • Role-based access control

These authentication methods can be combined with other Echo middleware like rate limiting, CORS, and request logging to create a comprehensive security solution for your applications.

Remember that security is a complex topic, and authentication is just one part of a comprehensive security strategy. Always keep your dependencies updated and follow security best practices.

Additional Resources

Exercises

  1. Implement a middleware that checks both JWT authentication and API key for extra security.
  2. Create a middleware that validates token expiration and automatically rejects expired tokens.
  3. Implement a logout functionality that invalidates tokens.
  4. Add rate limiting to the login route to prevent brute force attacks.
  5. Set up a database to store user credentials and authenticate against it instead of using hardcoded values.


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