Echo Secured Resources
Introduction
When building web applications, one of the most important aspects is security, particularly controlling access to certain resources or API endpoints. In this guide, we'll explore how to secure resources in Echo, a high-performance, extensible, and minimalist web framework for Go.
Securing resources involves two main concepts:
- Authentication - Verifying the identity of a user/client
- Authorization - Determining if an authenticated user has permission to access a resource
Echo provides several built-in mechanisms and middleware options that make it easy to implement both authentication and authorization in your applications.
Basic Authentication
One of the simplest forms of authentication is HTTP Basic Authentication. Echo provides built-in middleware to handle this.
Implementation Example
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
// Basic auth middleware
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {
// Check if username and password are correct
if username == "john" && password == "secret" {
return true, nil
}
return false, nil
}))
// Protected route
e.GET("/profile", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to your profile!")
})
e.Logger.Fatal(e.Start(":1323"))
}
When a user tries to access the /profile
route, they will be prompted to enter a username and password. Only if they enter the correct credentials ("john"/"secret") will they be allowed to access the route.
JWT Authentication
JSON Web Tokens (JWT) are a more sophisticated way to handle authentication, especially for RESTful APIs and single-page applications.
Step-by-step Implementation
- First, let's install the JWT middleware:
go get github.com/labstack/echo/v4/middleware
- Create a JWT-based authentication system:
package main
import (
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"time"
)
type jwtCustomClaims struct {
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 (in a real app, this should check against a database)
if username != "john" || password != "secret" {
return echo.ErrUnauthorized
}
// Set custom claims
claims := &jwtCustomClaims{
Name: "John Doe",
Admin: true,
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([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
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()
// 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"))
}
- Test your JWT authentication:
# Login and get a token
curl -X POST -d "username=john&password=secret" http://localhost:1323/login
# Access a restricted route with the token
curl http://localhost:1323/restricted -H "Authorization: Bearer <your_token_here>"
Role-Based Access Control
In many applications, different users have different roles and permissions. Let's implement a simple role-based access control system.
package main
import (
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
type jwtCustomClaims struct {
Name string `json:"name"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
// Middleware to check if user has required role
func requireRole(role string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
// Check if user has the required role
for _, r := range claims.Roles {
if r == role {
return next(c)
}
}
return echo.ErrForbidden
}
}
}
func main() {
e := echo.New()
// Configure JWT middleware
config := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("secret"),
}
// Protected routes with role-based access
admin := e.Group("/admin")
admin.Use(middleware.JWTWithConfig(config))
admin.Use(requireRole("admin"))
admin.GET("", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the admin panel!")
})
user := e.Group("/user")
user.Use(middleware.JWTWithConfig(config))
user.Use(requireRole("user"))
user.GET("", func(c echo.Context) error {
return c.String(http.StatusOK, "Welcome to the user dashboard!")
})
e.Logger.Fatal(e.Start(":1323"))
}
Custom Authorization Middleware
You can create your own custom middleware for more complex authorization logic. Here's an example that checks if a user is authorized to access a specific resource:
package main
import (
"github.com/labstack/echo/v4"
"net/http"
)
// Resource represents a protected item
type Resource struct {
ID string
Owner string
}
// Our "database" of resources
var resources = map[string]Resource{
"1": {ID: "1", Owner: "john"},
"2": {ID: "2", Owner: "jane"},
}
// Middleware to check resource ownership
func resourceOwnerOnly() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// In a real app, you would get the user ID from the token
userID := c.Request().Header.Get("X-User-ID")
resourceID := c.Param("id")
// Lookup the resource
resource, exists := resources[resourceID]
if !exists {
return echo.NewHTTPError(http.StatusNotFound, "Resource not found")
}
// Check if the user is the owner
if resource.Owner != userID {
return echo.NewHTTPError(http.StatusForbidden, "You don't have permission to access this resource")
}
return next(c)
}
}
}
func main() {
e := echo.New()
// Route with resource ownership check
e.GET("/resources/:id", func(c echo.Context) error {
id := c.Param("id")
return c.JSON(http.StatusOK, resources[id])
}, resourceOwnerOnly())
e.Logger.Fatal(e.Start(":1323"))
}
To test this, you would make requests like:
# This should succeed (john accessing resource 1)
curl -H "X-User-ID: john" http://localhost:1323/resources/1
# This should fail (john trying to access resource 2, which belongs to jane)
curl -H "X-User-ID: john" http://localhost:1323/resources/2
CORS Security
Cross-Origin Resource Sharing (CORS) is another important security aspect when building APIs. Echo provides middleware to handle CORS:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
// CORS middleware
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"https://yourtrustedwebsite.com"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
AllowHeaders: []string{echo.HeaderOrigin, echo.HeaderContentType, echo.HeaderAccept, echo.HeaderAuthorization},
}))
// Protected route
e.GET("/api/data", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "This is protected data",
})
})
e.Logger.Fatal(e.Start(":1323"))
}
Rate Limiting for API Protection
To protect your API from abuse or DoS attacks, you can implement rate limiting:
package main
import (
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
)
func main() {
e := echo.New()
// Rate limiter: 10 requests per second with burst of 30
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(10)))
e.GET("/api", func(c echo.Context) error {
return c.String(http.StatusOK, "API response")
})
e.Logger.Fatal(e.Start(":1323"))
}
Real-World Example: Securing a Blog API
Let's put everything together in a more realistic example of a blog API with different levels of security:
package main
import (
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"net/http"
"time"
)
type User struct {
ID string
Username string
Password string
Roles []string
}
type Post struct {
ID string
Title string
Content string
Author string
}
// Mock database
var users = map[string]User{
"1": {ID: "1", Username: "admin", Password: "admin123", Roles: []string{"admin", "writer"}},
"2": {ID: "2", Username: "writer", Password: "writer123", Roles: []string{"writer"}},
"3": {ID: "3", Username: "reader", Password: "reader123", Roles: []string{"reader"}},
}
var posts = map[string]Post{
"1": {ID: "1", Title: "First Post", Content: "Content of first post", Author: "1"},
"2": {ID: "2", Title: "Second Post", Content: "Content of second post", Author: "2"},
}
type jwtCustomClaims struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Find user by username
var user User
var foundUser bool
for _, u := range users {
if u.Username == username {
user = u
foundUser = true
break
}
}
// Check if user exists and password is correct
if !foundUser || user.Password != password {
return echo.ErrUnauthorized
}
// Create token
claims := &jwtCustomClaims{
UserID: user.ID,
Roles: user.Roles,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
t, err := token.SignedString([]byte("secret"))
if err != nil {
return err
}
return c.JSON(http.StatusOK, echo.Map{
"token": t,
})
}
// Middleware to check if user has any of the required roles
func requireAnyRole(roles ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
for _, requiredRole := range roles {
for _, userRole := range claims.Roles {
if requiredRole == userRole {
return next(c)
}
}
}
return echo.ErrForbidden
}
}
}
// Middleware to check if user is the owner of a post or an admin
func canModifyPost() echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
postID := c.Param("id")
post, exists := posts[postID]
if !exists {
return echo.NewHTTPError(http.StatusNotFound, "Post not found")
}
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
// Check if user is admin or author of the post
isAdmin := false
for _, role := range claims.Roles {
if role == "admin" {
isAdmin = true
break
}
}
if isAdmin || post.Author == claims.UserID {
return next(c)
}
return echo.ErrForbidden
}
}
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// CORS
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: []string{"*"},
AllowMethods: []string{http.MethodGet, http.MethodPut, http.MethodPost, http.MethodDelete},
}))
// Rate limiting
e.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(20)))
// Public routes
e.POST("/login", login)
// JWT middleware
jwtConfig := middleware.JWTConfig{
Claims: &jwtCustomClaims{},
SigningKey: []byte("secret"),
}
// API routes - all require authentication
api := e.Group("/api")
api.Use(middleware.JWTWithConfig(jwtConfig))
// Routes accessible by readers, writers, and admins
api.GET("/posts", func(c echo.Context) error {
return c.JSON(http.StatusOK, posts)
}, requireAnyRole("reader", "writer", "admin"))
api.GET("/posts/:id", func(c echo.Context) error {
id := c.Param("id")
post, exists := posts[id]
if !exists {
return echo.NewHTTPError(http.StatusNotFound, "Post not found")
}
return c.JSON(http.StatusOK, post)
}, requireAnyRole("reader", "writer", "admin"))
// Routes accessible by writers and admins
api.POST("/posts", func(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*jwtCustomClaims)
post := new(Post)
if err := c.Bind(post); err != nil {
return err
}
// Generate a new ID (in a real app, this would be more sophisticated)
post.ID = "3"
post.Author = claims.UserID
// Save the post
posts[post.ID] = *post
return c.JSON(http.StatusCreated, post)
}, requireAnyRole("writer", "admin"))
// Routes that require ownership or admin rights
api.PUT("/posts/:id", func(c echo.Context) error {
id := c.Param("id")
post, exists := posts[id]
if !exists {
return echo.NewHTTPError(http.StatusNotFound, "Post not found")
}
updatedPost := new(Post)
if err := c.Bind(updatedPost); err != nil {
return err
}
// Update fields but keep the author and ID
post.Title = updatedPost.Title
post.Content = updatedPost.Content
posts[id] = post
return c.JSON(http.StatusOK, post)
}, canModifyPost())
api.DELETE("/posts/:id", func(c echo.Context) error {
id := c.Param("id")
if _, exists := posts[id]; !exists {
return echo.NewHTTPError(http.StatusNotFound, "Post not found")
}
delete(posts, id)
return c.NoContent(http.StatusNoContent)
}, canModifyPost())
// Admin-only routes
admin := api.Group("/admin")
admin.Use(requireAnyRole("admin"))
admin.GET("/users", func(c echo.Context) error {
return c.JSON(http.StatusOK, users)
})
e.Logger.Fatal(e.Start(":1323"))
}
Summary
In this guide, we've covered several approaches to securing resources in Echo applications:
- Basic Authentication - Simple username/password protection
- JWT Authentication - Token-based authentication for more complex applications
- Role-Based Access Control - Restricting access based on user roles
- Custom Authorization Middleware - Building your own authorization logic
- CORS Security - Protecting your API from unauthorized origins
- Rate Limiting - Preventing abuse of your API
We also combined these concepts into a real-world example of a blog API with different levels of security for different endpoints and actions.
Remember these key principles when securing your Echo applications:
- Always authenticate users before checking authorization
- Use HTTPS in production to prevent token theft
- Store passwords securely (we used plaintext in our examples for simplicity, but in real applications, always hash passwords)
- Apply the principle of least privilege - users should only have access to what they need
Additional Resources
Exercises
- Implement an OAuth2 authentication system for your Echo application.
- Create a middleware that logs all access attempts to protected resources.
- Implement a more sophisticated role-based system with hierarchical roles.
- Add IP-based blocking for suspicious activity.
- Create a two-factor authentication system using SMS or authenticator apps.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)