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:
- Basic knowledge of Go programming
- Echo framework installed
- Understanding of HTTP and authentication concepts
Setting Up Your Project
Start by creating a new project and installing Echo:
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:
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:
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:
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:
# 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:
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:
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:
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
-
Always use HTTPS: Transmit authentication credentials only over secure connections.
-
Secure API Keys: Store API keys securely and transmit them using headers rather than URL parameters.
-
Proper Error Messages: Don't reveal too much information in error messages that could help attackers.
-
Rate Limiting: Implement rate limiting to prevent brute force attacks:
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)
}
}
}
- Logging: Log authentication attempts for security auditing:
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
- Echo Framework Documentation
- OWASP Authentication Best Practices
- JWT Authentication in Echo
- OAuth 2.0 Documentation
Exercises
-
Create a custom authentication middleware that validates JWT tokens without using Echo's built-in JWT middleware.
-
Implement a session-based authentication system using cookies and a Redis store for session data.
-
Extend the role-based middleware to support permission-based access control, where users can have specific permissions rather than just roles.
-
Build a complete authentication system with registration, login, and password reset functionality.
-
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! :)