Skip to content

Commit 079e2d2

Browse files
authored
🐛 Enforce HS256-only JWT parsing via shared parser with WithValidMethods (#4356)
All JWT parsing now goes through middleware.ParseJWT() which uses a shared jwt.Parser configured with jwt.WithValidMethods([]string{"HS256"}). Previously, ParseWithClaims was called without algorithm restriction — the library's default accepts any signing method. This could theoretically allow algorithm confusion attacks (e.g., HS384, RS256-with-HMAC-key). Defense-in-depth: the keyfunc also explicitly checks token.Method is *jwt.SigningMethodHMAC before returning the secret. Four call sites consolidated: - JWTAuth middleware (HTTP API) - ValidateJWT (WebSocket/exec) - RefreshToken handler - Logout handler Signed-off-by: Andrew Anderson <andy@clubanderson.com>
1 parent 08edb2d commit 079e2d2

File tree

2 files changed

+24
-13
lines changed

2 files changed

+24
-13
lines changed

pkg/api/handlers/auth.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -460,9 +460,7 @@ func (h *AuthHandler) Logout(c *fiber.Ctx) error {
460460
if tokenString == "" {
461461
return fiber.NewError(fiber.StatusUnauthorized, "Missing authorization")
462462
}
463-
token, err := jwt.ParseWithClaims(tokenString, &middleware.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
464-
return []byte(h.jwtSecret), nil
465-
})
463+
token, err := middleware.ParseJWT(tokenString, h.jwtSecret)
466464
if err != nil {
467465
return fiber.NewError(fiber.StatusUnauthorized, "Invalid token")
468466
}
@@ -511,10 +509,7 @@ func (h *AuthHandler) RefreshToken(c *fiber.Ctx) error {
511509
if tokenString == "" {
512510
return fiber.NewError(fiber.StatusUnauthorized, "Missing authorization")
513511
}
514-
token, err := jwt.ParseWithClaims(tokenString, &middleware.UserClaims{}, func(token *jwt.Token) (interface{}, error) {
515-
return []byte(h.jwtSecret), nil
516-
})
517-
512+
token, err := middleware.ParseJWT(tokenString, h.jwtSecret)
518513
if err != nil {
519514
return fiber.NewError(fiber.StatusUnauthorized, "Invalid token")
520515
}

pkg/api/middleware/auth.go

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,26 @@ type UserClaims struct {
2929
jwt.RegisteredClaims
3030
}
3131

32+
// jwtParser is a shared parser configured to accept only HS256.
33+
// This prevents algorithm confusion attacks where an attacker crafts a token
34+
// with a different signing method (e.g., "none", RS256 with HMAC key).
35+
// See: https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/
36+
var jwtParser = jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}))
37+
38+
// ParseJWT parses and validates a JWT token using the shared HS256-only parser.
39+
// All JWT validation in the codebase should use this function (or the JWTAuth
40+
// middleware which calls it) to ensure consistent algorithm enforcement.
41+
func ParseJWT(tokenString string, secret string) (*jwt.Token, error) {
42+
return jwtParser.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
43+
// Defense-in-depth: verify signing method is HMAC even though the parser
44+
// already restricts to HS256 via WithValidMethods.
45+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
46+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
47+
}
48+
return []byte(secret), nil
49+
})
50+
}
51+
3252
// TokenRevoker is the subset of store.Store needed for token revocation.
3353
// Defined here to avoid a circular import with the store package.
3454
type TokenRevoker interface {
@@ -184,9 +204,7 @@ func JWTAuth(secret string) fiber.Handler {
184204
return fiber.NewError(fiber.StatusUnauthorized, "Missing authorization")
185205
}
186206

187-
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
188-
return []byte(secret), nil
189-
})
207+
token, err := ParseJWT(tokenString, secret)
190208

191209
if err != nil {
192210
log.Printf("[Auth] Token parse error for %s: %v", c.Path(), err)
@@ -265,9 +283,7 @@ var ErrTokenRevoked = fmt.Errorf("token has been revoked")
265283
// This performs the same revocation check as the HTTP JWTAuth middleware
266284
// so that revoked tokens are rejected on WebSocket/exec paths too (#3894).
267285
func ValidateJWT(tokenString, secret string) (*UserClaims, error) {
268-
token, err := jwt.ParseWithClaims(tokenString, &UserClaims{}, func(token *jwt.Token) (interface{}, error) {
269-
return []byte(secret), nil
270-
})
286+
token, err := ParseJWT(tokenString, secret)
271287

272288
if err != nil {
273289
return nil, err

0 commit comments

Comments
 (0)