I'm working on an Echo-based Go application where I'm using JWT for authentication. I've integrated the echo-jwt middleware and oapi-codegen for OpenAPI validation. However, I'm facing issues where both the middleware and the protected route handler return 401 responses. This results in the following responses:
{"code": 401,"message": "invalid token"}
{"code": 401,"message": "missing or malformed JWT"}
The first 401 response is from the middleware and the second from the protected route. It seems like the middleware is not properly stopping the request when the token is invalid or missing, causing the route handler to execute and return another 401 error.
Here's the structure of my main function and middleware:
Main Function
package main
import (
"context"
"database/sql"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"auth-service/internal/application"
"auth-service/internal/handlers"
"auth-service/internal/middleware"
"auth-service/internal/services/auth"
"auth-service/pkg/database"
"auth-service/pkg/ent"
"auth-service/pkg/openapi"
"github.com/labstack/echo/v4"
echoMiddleware "github.com/labstack/echo/v4/middleware"
)
func main() {
startPprofServer()
e := setupEcho()
db, client := setupDatabase()
defer db.Close()
defer client.Close()
env := loadEnv()
secretKey := env["SECRET_KEY"]
tokenBlacklist := auth.NewTokenBlacklist(client)
authService := auth.NewAuthService(client, secretKey, tokenBlacklist)
// Initialize services and handlers
services := initServices(authService)
handlers := initHandlers(services)
strictHandler := openapi.NewStrictHandler(handlers, nil)
registerRoutes(e, strictHandler, tokenBlacklist, secretKey)
// Start server with graceful shutdown
go func() {
if err := e.Start(":8080"); err != nil && err != http.ErrServerClosed {
e.Logger.Fatal("shutting down the server")
}
}()
gracefulShutdown(e)
}
func startPprofServer() {
go func() {
log.Println("Starting pprof server on :6060")
if err := http.ListenAndServe("0.0.0.0:6060", nil); err != nil {
log.Fatalf("Failed to start pprof server: %v", err)
}
}()
}
func setupEcho() *echo.Echo {
e := echo.New()
e.Use(echoMiddleware.Logger())
e.Use(echoMiddleware.Recover())
e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{
AllowOrigins: []string{"http://localhost:9000"},
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete},
}))
e.HTTPErrorHandler = customHTTPErrorHandler
return e
}
func setupDatabase() (*sql.DB, *ent.Client) {
db, err := database.OpenDB()
handleError(err)
client, err := database.NewEntClient(db)
handleError(err)
database.ResetDatabase(db, client)
return db, client
}
func handleError(err error) {
if err != nil {
log.Fatal(err)
}
}
func loadEnv() map[string]string {
env := map[string]string{
"SECRET_KEY": os.Getenv("SECRET_KEY"),
}
for key, value := range env {
if value == "" {
log.Fatalf("%s environment variable is not set", key)
}
}
return env
}
func initServices(authService *auth.AuthService) map[string]interface{} {
return map[string]interface{}{
"postLogin": application.NewPostLogin(authService),
"postRegister": application.NewPostRegister(authService),
"changeEmail": application.NewChangeEmail(authService),
"refreshToken": application.NewRefreshToken(authService),
"updatePassword": application.NewUpdatePassword(authService),
"logout": application.NewLogout(authService),
}
}
func initHandlers(services map[string]interface{}) *handlers.CompositeHandler {
return handlers.NewCompositeHandler(
handlers.NewPostLoginHandler(services["postLogin"].(*application.PostLogin)),
handlers.NewPostRegisterHandler(services["postRegister"].(*application.PostRegister)),
handlers.NewChangeEmailHandler(services["changeEmail"].(*application.ChangeEmail)),
handlers.NewRefreshTokenHandler(services["refreshToken"].(*application.RefreshToken)),
handlers.NewUpdatePasswordHandler(services["updatePassword"].(*application.UpdatePassword)),
handlers.NewLogoutHandler(services["logout"].(*application.Logout)),
)
}
func registerRoutes(e *echo.Echo, strictHandler openapi.ServerInterface, tokenBlacklist *auth.TokenBlacklist, secretKey string) {
jwtMiddleware := middleware.NewJWTMiddleware(secretKey, tokenBlacklist)
e.POST("/login", func(c echo.Context) error {
return strictHandler.PostLogin(c)
})
e.POST("/register", func(c echo.Context) error {
return strictHandler.PostRegister(c)
})
e.POST("/refresh-token", func(c echo.Context) error {
return strictHandler.PostRefreshToken(c)
})
protected := e.Group("/auth")
protected.Use(jwtMiddleware.Handler())
protected.POST("/change-email", func(c echo.Context) error {
return strictHandler.PostChangeEmail(c)
})
protected.POST("/update-password", func(c echo.Context) error {
return strictHandler.PostUpdatePassword(c)
})
protected.POST("/logout", func(c echo.Context) error {
params := openapi.PostLogoutParams{
RefreshToken: c.Request().Header.Get("Refresh-Token"),
}
return strictHandler.PostLogout(c, params)
})
}
func customHTTPErrorHandler(err error, c echo.Context) {
code := http.StatusInternalServerError
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
}
if code == http.StatusNotFound {
c.JSON(http.StatusNotFound, map[string]string{
"message": "Not Found",
})
} else {
c.JSON(code, map[string]string{
"message": err.Error(),
})
}
}
func gracefulShutdown(e *echo.Echo) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := e.Shutdown(ctx); err != nil {
e.Logger.Fatal(err)
}
}
JWT Middleware
package middleware
import (
"context"
"net/http"
"auth-service/internal/customresponses"
"auth-service/internal/services/auth"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4"
)
type JWTMiddleware struct {
SecretKey string
TokenBlacklist *auth.TokenBlacklist
}
func NewJWTMiddleware(secretKey string, tokenBlacklist *auth.TokenBlacklist) *JWTMiddleware {
return &JWTMiddleware{
SecretKey: secretKey,
TokenBlacklist: tokenBlacklist,
}
}
func (m *JWTMiddleware) Handler() echo.MiddlewareFunc {
config := echojwt.Config{
SigningKey: []byte(m.SecretKey),
ErrorHandler: func(c echo.Context, err error) error {
var message string
if err == echojwt.ErrJWTMissing {
message = "missing or malformed JWT"
} else {
message = "invalid token"
}
// Return the response and stop further execution
return c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, message))
},
SuccessHandler: func(c echo.Context) {
userToken, ok := c.Get("user").(*jwt.Token)
if !ok {
// Return the response and stop further execution
c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, "invalid token"))
c.Response().Flush()
return
}
tokenString := userToken.Raw
isBlacklisted, err := m.TokenBlacklist.IsBlacklisted(tokenString)
if err != nil {
// Return the response and stop further execution
c.JSON(http.StatusInternalServerError, customresponses.CreateErrorResponse(http.StatusInternalServerError, "internal server error"))
c.Response().Flush()
return
}
if isBlacklisted {
// Return the response and stop further execution
c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, "invalid token"))
c.Response().Flush()
return
}
// Store the Echo context in the request context
c.SetRequest(c.Request().WithContext(context.WithValue(c.Request().Context(), "echoContext", c)))
},
ContinueOnIgnoredError: false, // Ensure this is set to false to stop the request on error
}
return echojwt.WithConfig(config)
}
Customresponses
package customresponses
import ( "auth-service/pkg/openapi" )
// CreateErrorResponse creates a structured error response func CreateErrorResponse(code int, message string) openapi.ErrorResponse { return openapi.ErrorResponse{ Code: &code, Message: &message, } }
When I call a protected route, I receive two 401 responses: one from the middleware and one from the protected route handler. It appears that the middleware is not stoppingthe request when an error occurs, allowing the route handler to also execute and return another 401 error. I have tried various solutions, but the problem persists. How can I ensure that the middleware properly halts the execution flow when an error is detected?
What I've Tried
Configured the JWT Middleware:
Set ContinueOnIgnoredError to false to stop further execution on error. Used custom error handling to differentiate between missing and invalid tokens. Ensured Middleware Order:
Applied the middleware before protected routes.
Proper Error Handling:
- Tried to return responses and stop further execution in the error handler. Expected Behavior When a request with an invalid or missing JWT is made to a protected route, the middleware should return a 401 response and stop further execution, preventing the protected route handler from being called.
Actual Behavior:
- The middleware returns a 401 response for invalid or missing JWTs, but the protected route handler also gets called and returns another 401 response, resulting in multiple 401 responses for a single request.
Please help me, I'm a beginner and I really want to resolve the issue where both the middleware and the protected route handler return 401 responses, resulting in multiple 401 responses for a single request.
How can I ensure that the JWT middleware properly halts execution and prevents the route handler from being called when the token is invalid or missing? Any insights or suggestions would be greatly appreciated.