Skip to content

Commit 4fceb9a

Browse files
authored
chore: migrate to golang-jwt v5 and update token error handling (#348)
- Upgrade github.com/golang-jwt/jwt dependency from v4 to v5 - Update token expiration error handling to use new v5 error types - Add logging for tokens with invalid claims - Adjust test assertions to match updated error messages from jwt v5 - Improve handling of JWT exp claim errors, distinguishing between expired, missing, and invalid type cases - Ensure ParseOptions always include WithTimeFunc for correct time validation - Fix typo in exp claim comment - Add a check for missing exp claim for backwards compatibility - Update tests to verify correct error messages and status codes for missing or invalid exp claims - Add new tests for required exp claim and invalid exp format scenarios fixed: #332 Co-Author: @gblandinkingland
1 parent 3bf5c41 commit 4fceb9a

File tree

4 files changed

+76
-32
lines changed

4 files changed

+76
-32
lines changed

auth_jwt.go

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@ package jwt
22

33
import (
44
"crypto/rsa"
5-
"encoding/json"
65
"errors"
76
"net/http"
87
"os"
98
"strings"
109
"time"
1110

1211
"github.com/gin-gonic/gin"
13-
"github.com/golang-jwt/jwt/v4"
12+
"github.com/golang-jwt/jwt/v5"
1413
"github.com/youmark/pkcs8"
1514
)
1615

@@ -154,10 +153,11 @@ type GinJWTMiddleware struct {
154153
// CookieSameSite allow use http.SameSite cookie param
155154
CookieSameSite http.SameSite
156155

157-
// ParseOptions allow to modify jwt's parser methods
156+
// ParseOptions allow to modify jwt's parser methods.
157+
// WithTimeFunc is always added to ensure the TimeFunc is propagated to the validator
158158
ParseOptions []jwt.ParserOption
159159

160-
// Default vaule is "exp"
160+
// Default value is "exp"
161161
ExpField string
162162
}
163163

@@ -446,6 +446,11 @@ func (mw *GinJWTMiddleware) MiddlewareInit() error {
446446
return ErrMissingSecretKey
447447
}
448448

449+
if len(mw.ParseOptions) == 0 {
450+
mw.ParseOptions = []jwt.ParserOption{}
451+
}
452+
mw.ParseOptions = append(mw.ParseOptions, jwt.WithTimeFunc(mw.TimeFunc))
453+
449454
return nil
450455
}
451456

@@ -459,31 +464,24 @@ func (mw *GinJWTMiddleware) MiddlewareFunc() gin.HandlerFunc {
459464
func (mw *GinJWTMiddleware) middlewareImpl(c *gin.Context) {
460465
claims, err := mw.GetClaimsFromJWT(c)
461466
if err != nil {
462-
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
463-
return
464-
}
465-
466-
switch v := claims[mw.ExpField].(type) {
467-
case nil:
468-
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
469-
return
470-
case float64:
471-
if int64(v) < mw.TimeFunc().Unix() {
467+
if errors.Is(err, jwt.ErrTokenExpired) {
472468
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
473469
return
474-
}
475-
case json.Number:
476-
n, err := v.Int64()
477-
if err != nil {
470+
} else if errors.Is(err, jwt.ErrInvalidType) && strings.Contains(err.Error(), "exp is invalid") {
478471
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
479472
return
480-
}
481-
if n < mw.TimeFunc().Unix() {
482-
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(ErrExpiredToken, c))
473+
} else if errors.Is(err, jwt.ErrTokenRequiredClaimMissing) && strings.Contains(err.Error(), "exp claim is required") {
474+
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
475+
return
476+
} else {
477+
mw.unauthorized(c, http.StatusUnauthorized, mw.HTTPStatusMessageFunc(err, c))
483478
return
484479
}
485-
default:
486-
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrWrongFormatOfExp, c))
480+
}
481+
482+
// For backwards compatibility since technically exp is not required in the spec but has been in gin-jwt
483+
if claims["exp"] == nil {
484+
mw.unauthorized(c, http.StatusBadRequest, mw.HTTPStatusMessageFunc(ErrMissingExpField, c))
487485
return
488486
}
489487

@@ -645,8 +643,7 @@ func (mw *GinJWTMiddleware) CheckIfTokenExpire(c *gin.Context) (jwt.MapClaims, e
645643
// If the error is just ValidationErrorExpired, we want to continue, as we can still
646644
// refresh the token if it's within the MaxRefresh time.
647645
// (see https://github.com/appleboy/gin-jwt/issues/176)
648-
validationErr, ok := err.(*jwt.ValidationError)
649-
if !ok || validationErr.Errors != jwt.ValidationErrorExpired {
646+
if !errors.Is(err, jwt.ErrTokenExpired) {
650647
return nil, err
651648
}
652649
}

auth_jwt_test.go

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414

1515
"github.com/appleboy/gofight/v2"
1616
"github.com/gin-gonic/gin"
17-
"github.com/golang-jwt/jwt/v4"
17+
"github.com/golang-jwt/jwt/v5"
1818
"github.com/stretchr/testify/assert"
1919
"github.com/tidwall/gjson"
2020
)
@@ -1233,7 +1233,7 @@ func TestExpiredField(t *testing.T) {
12331233
})
12341234

12351235
// wrong format
1236-
claims["exp"] = "wrongFormatForExpiryIgnoredByJwtLibrary"
1236+
claims["exp"] = "wrongFormatForExpiry"
12371237
tokenString, _ = token.SignedString(key)
12381238

12391239
r.GET("/auth/hello").
@@ -1243,8 +1243,55 @@ func TestExpiredField(t *testing.T) {
12431243
Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
12441244
message := gjson.Get(r.Body.String(), "message")
12451245

1246-
assert.Equal(t, ErrExpiredToken.Error(), strings.ToLower(message.String()))
1247-
assert.Equal(t, http.StatusUnauthorized, r.Code)
1246+
assert.Equal(t, ErrWrongFormatOfExp.Error(), strings.ToLower(message.String()))
1247+
assert.Equal(t, http.StatusBadRequest, r.Code)
1248+
})
1249+
}
1250+
1251+
func TestExpiredFieldRequiredParserOption(t *testing.T) {
1252+
// the middleware to test
1253+
authMiddleware, _ := New(&GinJWTMiddleware{
1254+
Realm: "test zone",
1255+
Key: key,
1256+
Timeout: time.Hour,
1257+
Authenticator: defaultAuthenticator,
1258+
ParseOptions: []jwt.ParserOption{jwt.WithExpirationRequired()},
1259+
})
1260+
1261+
handler := ginHandler(authMiddleware)
1262+
1263+
r := gofight.New()
1264+
1265+
token := jwt.New(jwt.GetSigningMethod("HS256"))
1266+
claims := token.Claims.(jwt.MapClaims)
1267+
claims["identity"] = "admin"
1268+
claims["orig_iat"] = 0
1269+
tokenString, _ := token.SignedString(key)
1270+
1271+
r.GET("/auth/hello").
1272+
SetHeader(gofight.H{
1273+
"Authorization": "Bearer " + tokenString,
1274+
}).
1275+
Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
1276+
message := gjson.Get(r.Body.String(), "message")
1277+
1278+
assert.Equal(t, ErrMissingExpField.Error(), message.String())
1279+
assert.Equal(t, http.StatusBadRequest, r.Code)
1280+
})
1281+
1282+
// wrong format
1283+
claims["exp"] = "wrongFormatForExpiry"
1284+
tokenString, _ = token.SignedString(key)
1285+
1286+
r.GET("/auth/hello").
1287+
SetHeader(gofight.H{
1288+
"Authorization": "Bearer " + tokenString,
1289+
}).
1290+
Run(handler, func(r gofight.HTTPResponse, rq gofight.HTTPRequest) {
1291+
message := gjson.Get(r.Body.String(), "message")
1292+
1293+
assert.Equal(t, ErrWrongFormatOfExp.Error(), strings.ToLower(message.String()))
1294+
assert.Equal(t, http.StatusBadRequest, r.Code)
12481295
})
12491296
}
12501297

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ go 1.23.0
55
require (
66
github.com/appleboy/gofight/v2 v2.1.2
77
github.com/gin-gonic/gin v1.10.1
8-
github.com/golang-jwt/jwt/v4 v4.5.2
8+
github.com/golang-jwt/jwt/v5 v5.2.3
99
github.com/stretchr/testify v1.10.0
1010
github.com/tidwall/gjson v1.17.1
1111
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO
2828
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
2929
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
3030
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
31-
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
32-
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
31+
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
32+
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
3333
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
3434
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3535
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

0 commit comments

Comments
 (0)