1

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.

2 Answers 2

1

@brits I already have and i still have the issue :

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
            c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, message))
            return echo.NewHTTPError(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)
}

I found a temporary solution by ignoring the middleware and setup directly in my main :

func setupEcho(authService *auth.AuthService) *echo.Echo {
    e := echo.New()

    swagger, err := openapi.GetSwagger()
    if err != nil {
        log.Fatalf("Failed to get swagger: %v", err)
    }

    options := &oapimiddleware.Options{
        SilenceServersWarning: true,
        Options: openapi3filter.Options{
            AuthenticationFunc: func(ctx context.Context, input *openapi3filter.AuthenticationInput) error {
                return authenticateRequest(ctx, input, authService)
            },
        },
    }

    e.Use(oapimiddleware.OapiRequestValidatorWithOptions(swagger, options))
    e.HTTPErrorHandler = customHTTPErrorHandler
    return e
}

By using :

oapimiddleware "github.com/oapi-codegen/echo-middleware"

Full code :

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "strings"
    "syscall"
    "time"

    "auth-service/internal/application"
    "auth-service/internal/handlers"
    "auth-service/internal/middleware"
    "auth-service/internal/server"
    "auth-service/internal/services/auth"
    "auth-service/pkg/database"
    "auth-service/pkg/ent"
    "auth-service/pkg/openapi"

    "github.com/getkin/kin-openapi/openapi3filter"
    "github.com/labstack/echo/v4"
    echoMiddleware "github.com/labstack/echo/v4/middleware"
    oapimiddleware "github.com/oapi-codegen/echo-middleware"
)

func main() {
    startPprofServer()

    db, client := database.SetupDatabase()
    defer db.Close()
    defer client.Close()

    env := server.LoadEnv()
    secretKey := env["SECRET_KEY"]
    tokenBlacklist, authService := initializeAuthService(client, secretKey)

    e := setupEcho(authService)

    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},
    }))

    services := initServices(authService)
    handlers := initHandlers(services)
    strictHandler := openapi.NewStrictHandler(handlers, nil)

    registerRoutes(e, strictHandler, tokenBlacklist, secretKey)

    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(authService *auth.AuthService) *echo.Echo {
    e := echo.New()

    swagger, err := openapi.GetSwagger()
    if err != nil {
        log.Fatalf("Failed to get swagger: %v", err)
    }

    options := &oapimiddleware.Options{
        SilenceServersWarning: true,
        Options: openapi3filter.Options{
            AuthenticationFunc: func(ctx context.Context, input *openapi3filter.AuthenticationInput) error {
                return authenticateRequest(ctx, input, authService)
            },
        },
    }

    e.Use(oapimiddleware.OapiRequestValidatorWithOptions(swagger, options))
    e.HTTPErrorHandler = customHTTPErrorHandler
    return e
}

func initializeAuthService(client *ent.Client, secretKey string) (*auth.TokenBlacklist, *auth.AuthService) {
    tokenBlacklist := auth.NewTokenBlacklist(client)
    authService := auth.NewAuthService(client, secretKey, tokenBlacklist)
    return tokenBlacklist, authService
}

func authenticateRequest(ctx context.Context, input *openapi3filter.AuthenticationInput, authService *auth.AuthService) error {
    authHeader := input.RequestValidationInput.Request.Header.Get("Authorization")
    if authHeader == "" {
        return echo.NewHTTPError(http.StatusUnauthorized, "missing or invalid token")
    }

    parts := strings.Split(authHeader, " ")
    if len(parts) != 2 || parts[0] != "Bearer" {
        return echo.NewHTTPError(http.StatusUnauthorized, "invalid token format")
    }

    tokenString := parts[1]
    _, err := authService.ValidateToken(tokenString)
    if err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, err.Error())
    }

    return nil
}

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)
    }
}

New middleware :

package middleware

import (
    "context"
    "net/http"

    "auth-service/internal/customresponses"
    "auth-service/internal/services/auth"

    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 {
            if err != nil {
                c.Logger().Error(err)
                return c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, "invalid token"))
            }
            return nil
        },
        SuccessHandler: func(c echo.Context) {
            c.SetRequest(c.Request().WithContext(context.WithValue(c.Request().Context(), "echoContext", c)))
        },
        ContinueOnIgnoredError: false,
    }

    return echojwt.WithConfig(config)
}

I fixed the topic problem by using the library "github.com/oapi-codegen/echo-middleware"

Directly in my main

Thanks for your help

Sign up to request clarification or add additional context in comments.

1 Comment

Note that ContinueOnIgnoredError only applies to ErrorHandlerWithContext (which you don't use). Also calling echo.NewHTTPError will send a response (message) so there is no need to call c.JSON as well. Currently this does not really look like an answer (so perhaps reword it or incorporate the content into your question if you are looking for another answer). A minimal example would make answering easier (there is a lot of code here).
0

The docs say:

ErrorHandler defines a function which is executed when all lookups have been done and none of them passed Validator function. ErrorHandler is executed with last missing (ErrExtractionValueMissing) or an invalid key. It may be used to define a custom JWT error.

Note: when error handler swallows the error (returns nil) middleware continues handler chain execution towards handler. This is useful in cases when portion of your site/api is publicly accessible and has extra features for authorized users In that case you can use ErrorHandler to set default public JWT token value to request and continue with handler chain.

In your ErrorHandler you:

// Return the response and stop further execution
return c.JSON(http.StatusUnauthorized, customresponses.CreateErrorResponse(http.StatusUnauthorized, message))

c.JSON will return nil if the error is successfully serialized (which is what I'd expect is happening); so you are effectively swallowing the error (and chained handlers will run). To resolve this don't return nil.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.