# Adding HTTP Authentication and Authorization in Go Echo: A Clean Architecture Guide

> Implement HTTP authentication and authorization in Go Echo with JWT middleware while maintaining clean architecture. Secure your API without altering core layers.

- Repository: [manato/golang-clean-architecture](https://github.com/manakuro/golang-clean-architecture)
- Tags: how-to-guide
- Published: 2026-03-06

---

**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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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:

1. A public `/login` endpoint that validates credentials via the use-case layer and returns a signed JWT
2. Protected routes wrapped with `middleware.JWT`, which validates tokens and injects claims into `echo.Context`
3. 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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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`.

```go
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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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.

```go
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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/infrastructure/middleware/role.go).

```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:

```go
adminGroup := e.Group("/admin")
adminGroup.Use(jwtMw, RoleMiddleware("admin"))
adminGroup.GET("/reports", adminHandler)

```

## Key Files and Their Responsibilities

- **[`pkg/infrastructure/router/router.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/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`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/config/config.go)** – Loads the JWT signing secret and other environment variables, exposing them through the `config.C` singleton.
- **[`pkg/adapter/controller/auth.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/adapter/controller/auth.go)** *(new)* – Handles the `/login` endpoint, binding JSON requests and issuing signed JWTs after use-case validation.
- **[`pkg/adapter/controller/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/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`](https://github.com/manakuro/golang-clean-architecture/blob/main/cmd/app/main.go)** – Application entry point that initializes the Echo instance, loads configuration, and invokes `NewRouter`.
- **[`pkg/domain/model/user.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/pkg/domain/model/user.go)** – User entity that can be extended with a `Role` field 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.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/config/config.go) and reference it via `config.C.JWTSecret` in both the router and login controller.
- **Validation**: Use `middleware.JWTWithConfig` in [`router.go`](https://github.com/manakuro/golang-clean-architecture/blob/main/router.go) to protect route groups, validating tokens before they reach business logic.
- **Authorization**: Implement custom middleware that reads `echo.Context` claims 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`](https://github.com/manakuro/golang-clean-architecture/blob/main/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.