Adding HTTP Authentication and Authorization in Go Echo: A Clean Architecture Guide
To add HTTP authentication and authorization in Go Echo while preserving clean architecture, implement JWT middleware in the router layer, create a login controller that issues tokens, and optionally add role-based authorization middleware—all without touching the use-case or domain layers.
The manakuro/golang-clean-architecture repository demonstrates how to structure a Go application so that HTTP concerns remain isolated from business logic. Adding authentication and authorization to this codebase requires extending only the infrastructure and adapter layers, ensuring that use-cases remain pure Go logic without Echo dependencies.
Where Authentication Fits in Clean Architecture
In this repository, the infrastructure/router layer serves as the sole entry point for HTTP-specific concerns. The NewRouter function in router/router.go creates the Echo instance and wires middleware, while controllers delegate to use-cases. This design respects the dependency rule: outer layers may depend on inner layers, but never the reverse.
The typical authentication flow involves:
- A public
/loginendpoint that validates credentials via the use-case layer and returns a signed JWT - Protected routes wrapped with
middleware.JWT, which validates tokens and injects claims intoecho.Context - Optional custom authorization middleware that reads claims and enforces role-based access before reaching the controller
Because authentication logic lives only in the router and controller layers, the use-case and repository layers require no modifications to support JWT validation.
Implementing JWT Authentication in Echo
Configuring the Router with JWT Middleware
The first step is modifying NewRouter in pkg/infrastructure/router/router.go to apply JWT protection to specific route groups. Public endpoints like login remain unprotected, while business routes use middleware.JWTWithConfig.
func NewRouter(e *echo.Echo, c controller.AppController) *echo.Echo {
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Public endpoint – login returns a token
e.POST("/login", c.Auth.Login)
// JWT middleware – validates token and sets user context
jwtConfig := middleware.JWTConfig{
SigningKey: []byte(config.C.JWTSecret),
Claims: &jwt.CustomClaims{},
}
jwtMw := middleware.JWTWithConfig(jwtConfig)
// Protected routes
g := e.Group("")
g.Use(jwtMw) // all routes in this group need a valid JWT
g.GET("/users", func(ctx echo.Context) error { return c.User.GetUsers(ctx) })
g.POST("/users", func(ctx echo.Context) error { return c.User.CreateUser(ctx) })
return e
}
Store the JWT signing secret in pkg/config/config.go and expose it through the configuration struct to keep environment-specific values out of the router logic.
Creating the Login Controller
Create a new authentication controller (e.g., pkg/adapter/controller/auth.go) that handles credential validation and token issuance. This controller delegates password verification to the use-case layer, maintaining the boundary between HTTP and business logic.
func (ac *authController) Login(ctx controller.Context) error {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := ctx.Bind(&req); err != nil {
return ctx.JSON(http.StatusBadRequest, map[string]string{"error": "invalid payload"})
}
// Delegate credential check to the use‑case layer
user, err := ac.authUsecase.Authenticate(req.Email, req.Password)
if err != nil {
return ctx.JSON(http.StatusUnauthorized, map[string]string{"error": "unauthenticated"})
}
// Create JWT token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": user.ID,
"role": user.Role,
"exp": time.Now().Add(24 * time.Hour).Unix(),
})
signed, err := token.SignedString([]byte(config.C.JWTSecret))
if err != nil {
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": "token error"})
}
return ctx.JSON(http.StatusOK, map[string]string{"token": signed})
}
The Authenticate method in the use-case layer performs the actual password comparison against the repository, returning a domain model that includes the user's ID and role.
Adding Role-Based Authorization Middleware
For fine-grained access control, implement custom middleware that inspects JWT claims after the built-in JWT middleware has validated the token. Create this in a new file such as pkg/infrastructure/middleware/role.go.
func RoleMiddleware(requiredRole 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.(jwt.MapClaims)
if role, ok := claims["role"]; ok && role == requiredRole {
return next(c)
}
return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
}
}
}
Apply this middleware to specific route groups requiring elevated privileges:
adminGroup := e.Group("/admin")
adminGroup.Use(jwtMw, RoleMiddleware("admin"))
adminGroup.GET("/reports", adminHandler)
Key Files and Their Responsibilities
pkg/infrastructure/router/router.go– Configures the Echo instance, registers routes, and applies JWT middleware to protected groups. This is the only file that imports Echo-specific middleware.pkg/config/config.go– Loads the JWT signing secret and other environment variables, exposing them through theconfig.Csingleton.pkg/adapter/controller/auth.go(new) – Handles the/loginendpoint, binding JSON requests and issuing signed JWTs after use-case validation.pkg/adapter/controller/user.go– Existing user controller remains unchanged except for route wiring; it receives the authenticated context but contains no JWT logic.cmd/app/main.go– Application entry point that initializes the Echo instance, loads configuration, and invokesNewRouter.pkg/domain/model/user.go– User entity that can be extended with aRolefield to support authorization claims.
Summary
- Isolation: Keep JWT handling strictly within the router and controller layers so use-cases remain framework-agnostic.
- Configuration: Store the JWT secret in
config/config.goand reference it viaconfig.C.JWTSecretin both the router and login controller. - Validation: Use
middleware.JWTWithConfiginrouter.goto protect route groups, validating tokens before they reach business logic. - Authorization: Implement custom middleware that reads
echo.Contextclaims to enforce role-based access without polluting controllers. - Extensibility: This architecture allows the same authentication use-cases to serve gRPC or CLI entry points later without modification.
Frequently Asked Questions
How does clean architecture affect authentication implementation in Go?
Clean architecture requires that authentication mechanisms reside in the outer infrastructure layer. According to the manakuro/golang-clean-architecture source code, the use-case layer remains unaware of Echo or JWT specifics—it only knows how to validate credentials and return user entities. The router and controllers handle token extraction and validation, ensuring the dependency rule flows inward.
Where should JWT validation logic reside in an Echo application?
JWT validation belongs in the router configuration within pkg/infrastructure/router/router.go. Use Echo's middleware.JWTWithConfig to validate tokens before requests reach controllers. This centralizes HTTP security concerns in the infrastructure layer while keeping controllers focused on request/response mapping.
Can I use other authentication methods besides JWT with this approach?
Yes. The architecture supports Basic Auth, OAuth2, or session-based authentication by replacing or adding middleware in NewRouter. Since the use-case layer only receives validated principals through the controller context, you can swap JWT for middleware.BasicAuth or custom header validation without affecting business logic.
How do I handle token refresh in this architecture?
Implement a /refresh endpoint in the authentication controller that validates the existing token's claims (ignoring expiration via jwt.Parse options) and issues a new token with extended expiry. Keep this logic in the controller layer, delegating any refresh token storage or rotation policies to the use-case or repository layers as needed.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →