From f4ef71b35932f23196d6fdc7ae111d87e0fd8aa0 Mon Sep 17 00:00:00 2001 From: line-6000 <154492442+line-6000@users.noreply.github.com> Date: Fri, 22 May 2026 15:03:59 +0800 Subject: [PATCH] fix: accept bare admin JWT authorization header Allow the admin JWT middleware to parse either Authorization: Bearer or a bare JWT value, matching the existing comment. Add tests covering both accepted forms plus missing and empty authorization headers. --- src/middleware/check_jwt.go | 33 ++++++--- src/middleware/check_jwt_test.go | 118 +++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 src/middleware/check_jwt_test.go diff --git a/src/middleware/check_jwt.go b/src/middleware/check_jwt.go index 9428d81..0149bb5 100644 --- a/src/middleware/check_jwt.go +++ b/src/middleware/check_jwt.go @@ -27,16 +27,9 @@ const ( func CheckAdminJWT() echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(ctx echo.Context) error { - raw := strings.TrimSpace(ctx.Request().Header.Get("Authorization")) - if raw == "" { - return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header") - } - if !strings.HasPrefix(raw, "Bearer ") { - return echo.NewHTTPError(http.StatusUnauthorized, "authorization header must use Bearer scheme") - } - token := strings.TrimSpace(raw[len("Bearer "):]) - if token == "" { - return echo.NewHTTPError(http.StatusUnauthorized, "empty token") + token, err := adminTokenFromAuthorization(ctx.Request().Header.Get("Authorization")) + if err != nil { + return err } claims, err := appjwt.Parse(token) if err != nil { @@ -48,3 +41,23 @@ func CheckAdminJWT() echo.MiddlewareFunc { } } } + +func adminTokenFromAuthorization(header string) (string, error) { + raw := strings.TrimSpace(header) + if raw == "" { + return "", echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header") + } + + const bearerPrefix = "Bearer " + token := raw + if strings.HasPrefix(raw, bearerPrefix) { + token = strings.TrimSpace(raw[len(bearerPrefix):]) + } else if raw == strings.TrimSpace(bearerPrefix) { + token = "" + } + + if token == "" { + return "", echo.NewHTTPError(http.StatusUnauthorized, "empty token") + } + return token, nil +} diff --git a/src/middleware/check_jwt_test.go b/src/middleware/check_jwt_test.go new file mode 100644 index 0000000..7dfdb63 --- /dev/null +++ b/src/middleware/check_jwt_test.go @@ -0,0 +1,118 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/GMWalletApp/epusdt/internal/testutil" + "github.com/GMWalletApp/epusdt/model/data" + "github.com/GMWalletApp/epusdt/model/mdb" + appjwt "github.com/GMWalletApp/epusdt/util/jwt" + "github.com/labstack/echo/v4" +) + +func TestAdminTokenFromAuthorization(t *testing.T) { + tests := []struct { + name string + header string + want string + wantErr bool + }{ + { + name: "bearer token", + header: "Bearer jwt-token", + want: "jwt-token", + }, + { + name: "bare token", + header: "jwt-token", + want: "jwt-token", + }, + { + name: "trimmed bare token", + header: " jwt-token ", + want: "jwt-token", + }, + { + name: "missing header", + header: "", + wantErr: true, + }, + { + name: "empty bearer token", + header: "Bearer ", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := adminTokenFromAuthorization(tt.header) + if tt.wantErr { + if err == nil { + t.Fatal("expected error") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("token = %q, want %q", got, tt.want) + } + }) + } +} + +func TestCheckAdminJWTAcceptsBearerAndBareTokens(t *testing.T) { + cleanup := testutil.SetupTestDatabases(t) + defer cleanup() + + if err := data.SetSetting(mdb.SettingGroupSystem, mdb.SettingKeyJwtSecret, "test-jwt-secret", mdb.SettingTypeString); err != nil { + t.Fatalf("seed jwt secret: %v", err) + } + token, err := appjwt.Sign(42, "admin") + if err != nil { + t.Fatalf("sign jwt: %v", err) + } + + tests := []struct { + name string + header string + }{ + { + name: "bearer token", + header: "Bearer " + token, + }, + { + name: "bare token", + header: token, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := echo.New() + e.Use(CheckAdminJWT()) + e.GET("/", func(ctx echo.Context) error { + if got := ctx.Get(AdminUserIDKey); got != uint64(42) { + t.Fatalf("%s = %v, want 42", AdminUserIDKey, got) + } + if got := ctx.Get(AdminUsernameKey); got != "admin" { + t.Fatalf("%s = %v, want admin", AdminUsernameKey, got) + } + return ctx.NoContent(http.StatusNoContent) + }) + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.Header.Set(echo.HeaderAuthorization, tt.header) + rec := httptest.NewRecorder() + e.ServeHTTP(rec, req) + + if rec.Code != http.StatusNoContent { + t.Fatalf("status = %d, want %d; body=%s", rec.Code, http.StatusNoContent, rec.Body.String()) + } + }) + } +}