Skip to main content

Echo Custom Authentication

While Echo provides basic authentication mechanisms out of the box, many applications require custom authentication solutions tailored to specific business requirements. In this tutorial, you'll learn how to implement custom authentication in Echo to create flexible and secure authentication systems.

Introduction to Custom Authentication

Custom authentication allows you to define your own logic for authenticating users beyond the standard mechanisms like Basic Auth or JWT. This might include:

  • Custom token formats
  • Integration with third-party authentication services
  • Multi-factor authentication flows
  • Role-based access control
  • Session-based authentication

The core of custom authentication in Echo is implementing the echo.Middleware interface, which allows you to intercept and process requests before they reach your handlers.

Prerequisites

Before diving into custom authentication, make sure you have:

  1. Basic knowledge of Go programming
  2. Echo framework installed
  3. Understanding of HTTP and authentication concepts

Setting Up Your Project

Start by creating a new project and installing Echo:

bash
mkdir echo-custom-auth
cd echo-custom-auth
go mod init echo-custom-auth
go get github.com/labstack/echo/v4

Creating a Custom Authentication Middleware

Let's create a simple custom authentication middleware that verifies an API key from the request header:

go
package main

import (
"github.com/labstack/echo/v4"
"net/http"
)

// Define our custom auth middleware
func ApiKeyAuth(validApiKey string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get API key from header
apiKey := c.Request().Header.Get("X-API-Key")

// Check if the API key is valid
if apiKey != validApiKey {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid API key")
}

// If API key is valid, continue to the next handler
return next(c)
}
}
}

Creating Custom User Objects

You might want to store user information in the context after successful authentication:

go
type User struct {
ID string
Username string
Roles []string
}

func ApiKeyWithUserAuth(apiKeyToUser map[string]User) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
apiKey := c.Request().Header.Get("X-API-Key")

user, exists := apiKeyToUser[apiKey]
if !exists {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid API key")
}

// Store user in context for later use
c.Set("user", user)

return next(c)
}
}
}

Using the Custom Authentication Middleware

Here's how to use your custom authentication middleware in an Echo application:

go
package main

import (
"github.com/labstack/echo/v4"
"net/http"
)

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

// Define valid API key
validApiKey := "secret-api-key-1234"

// Public routes
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the public area!")
})

// Protected routes with custom auth middleware
protected := e.Group("/api")
protected.Use(ApiKeyAuth(validApiKey))

protected.GET("/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "You accessed protected data!",
})
})

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

Testing the Authentication

You can test your protected endpoints using curl:

bash
# This will fail with 401 Unauthorized
curl http://localhost:8080/api/data

# This will succeed
curl -H "X-API-Key: secret-api-key-1234" http://localhost:8080/api/data

Implementing Role-Based Access Control

Let's extend our authentication to include role-based access control:

go
func RoleMiddleware(requiredRole string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Get user from context (set by previous auth middleware)
user, ok := c.Get("user").(User)
if !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "User not found in context")
}

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

if !hasRole {
return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}

return next(c)
}
}
}

Real-World Example: Complete Authentication System

Here's a more comprehensive example that includes user lookup from a "database" and role-based access:

go
package main

import (
"github.com/labstack/echo/v4"
"net/http"
)

// User represents an authenticated user
type User struct {
ID string
Username string
Roles []string
}

// Mock user database
var userDB = map[string]User{
"api-key-admin": {
ID: "1",
Username: "admin",
Roles: []string{"admin", "user"},
},
"api-key-user": {
ID: "2",
Username: "regular_user",
Roles: []string{"user"},
},
}

// Custom authentication middleware
func AuthMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
apiKey := c.Request().Header.Get("X-API-Key")

user, exists := userDB[apiKey]
if !exists {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid API key")
}

// Store user in context
c.Set("user", user)

return next(c)
}
}
}

// Role checking middleware
func RequireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user, ok := c.Get("user").(User)
if !ok {
return echo.NewHTTPError(http.StatusUnauthorized, "User not found")
}

for _, r := range user.Roles {
if r == role {
return next(c)
}
}

return echo.NewHTTPError(http.StatusForbidden, "Insufficient permissions")
}
}
}

// Get current user utility function
func GetUser(c echo.Context) (User, error) {
user, ok := c.Get("user").(User)
if !ok {
return User{}, echo.NewHTTPError(http.StatusUnauthorized, "User not found")
}
return user, nil
}

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

// Public routes
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the API!")
})

// All authenticated routes
auth := e.Group("/api")
auth.Use(AuthMiddleware())

// Routes for all authenticated users
auth.GET("/profile", func(c echo.Context) error {
user, err := GetUser(c)
if err != nil {
return err
}

return c.JSON(http.StatusOK, map[string]interface{}{
"id": user.ID,
"username": user.Username,
"roles": user.Roles,
})
})

// Admin-only routes
admin := auth.Group("/admin")
admin.Use(RequireRole("admin"))

admin.GET("/stats", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]interface{}{
"users": 42,
"active_sessions": 12,
})
})

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

Working with Authentication Services

In real applications, you might integrate with authentication services like OAuth2, Firebase Auth, or Auth0. Here's a simplified example of integrating with a third-party service:

go
func ThirdPartyAuthMiddleware(authServiceURL string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
token := c.Request().Header.Get("Authorization")
if token == "" {
return echo.NewHTTPError(http.StatusUnauthorized, "Missing authorization token")
}

// Make HTTP request to validate token with third-party service
client := &http.Client{}
req, err := http.NewRequest("GET", authServiceURL+"/validate", nil)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Auth service error")
}

req.Header.Add("Authorization", token)
resp, err := client.Do(req)
if err != nil || resp.StatusCode != http.StatusOK {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid token")
}
defer resp.Body.Close()

// Parse response and set user in context
// ...

return next(c)
}
}
}

Best Practices for Custom Authentication

  1. Always use HTTPS: Transmit authentication credentials only over secure connections.

  2. Secure API Keys: Store API keys securely and transmit them using headers rather than URL parameters.

  3. Proper Error Messages: Don't reveal too much information in error messages that could help attackers.

  4. Rate Limiting: Implement rate limiting to prevent brute force attacks:

go
func RateLimiter(requestsPerMinute int) echo.MiddlewareFunc {
// Map to store IP addresses and their request counts
clients := make(map[string]int)
var mu sync.Mutex

// Reset counts every minute
go func() {
for {
time.Sleep(time.Minute)
mu.Lock()
clients = make(map[string]int)
mu.Unlock()
}
}()

return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
ip := c.RealIP()

mu.Lock()
count, exists := clients[ip]
if !exists {
clients[ip] = 1
} else {
clients[ip] = count + 1
}

if clients[ip] > requestsPerMinute {
mu.Unlock()
return echo.NewHTTPError(http.StatusTooManyRequests, "Rate limit exceeded")
}
mu.Unlock()

return next(c)
}
}
}
  1. Logging: Log authentication attempts for security auditing:
go
func LoggingMiddleware() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
start := time.Now()

// Process request
err := next(c)

// Log after request is processed
log.Printf(
"[%s] %s %s %d %s",
c.RealIP(),
c.Request().Method,
c.Request().URL.Path,
c.Response().Status,
time.Since(start),
)

return err
}
}
}

Summary

Custom authentication in Echo provides flexibility to implement security schemes that match your specific requirements. By creating middleware functions that implement the authentication logic, you can:

  • Validate custom tokens or API keys
  • Integrate with third-party authentication services
  • Implement role-based access control
  • Store user information in the request context
  • Apply fine-grained security policies

This approach makes Echo well-suited for applications with complex or specialized authentication needs.

Additional Resources

Exercises

  1. Create a custom authentication middleware that validates JWT tokens without using Echo's built-in JWT middleware.

  2. Implement a session-based authentication system using cookies and a Redis store for session data.

  3. Extend the role-based middleware to support permission-based access control, where users can have specific permissions rather than just roles.

  4. Build a complete authentication system with registration, login, and password reset functionality.

  5. Implement multi-factor authentication where users need both an API key and a time-based token to access protected endpoints.



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