Skip to content

Commit ca404ef

Browse files
authored
Merge pull request #55 from agilezebra/54-integer-precision-loss-when-decoding-jwt-payload-json
54 integer precision loss when decoding jwt payload json
2 parents d0c33c2 + 61317ba commit ca404ef

File tree

3 files changed

+77
-6
lines changed

3 files changed

+77
-6
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,15 @@ experimental:
1919
plugins:
2020
jwt:
2121
moduleName: github.com/agilezebra/jwt-middleware
22-
version: v1.2.18
22+
version: v1.2.19
2323
```
2424
1b. or with command-line options:
2525
2626
```yaml
2727
command:
2828
...
2929
- "--experimental.plugins.jwt.modulename=github.com/agilezebra/jwt-middleware"
30-
- "--experimental.plugins.jwt.version=v1.2.18"
30+
- "--experimental.plugins.jwt.version=v1.2.19"
3131
```
3232
3333
2) Configure and activate the plugin as a middleware in your dynamic traefik config:

jwt.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
172172
plugin := JWTPlugin{
173173
next: next,
174174
name: name,
175-
parser: jwt.NewParser(jwt.WithValidMethods(config.ValidMethods)),
175+
parser: jwt.NewParser(jwt.WithValidMethods(config.ValidMethods), jwt.WithJSONNumber()),
176176
secret: key,
177177
issuers: canonicalizeDomains(config.Issuers),
178178
clients: createClients(config.InsecureSkipVerify),
@@ -327,8 +327,7 @@ func (plugin *JWTPlugin) validate(request *http.Request, variables *TemplateVari
327327
if !plugin.validateClaim(claim, claims, requirements, variables) {
328328
err := fmt.Errorf("claim is not valid: %s", claim)
329329
// If the token is older than our freshness window, we allow that reauthorization might fix it
330-
iat, ok := claims["iat"]
331-
if ok && plugin.freshness != 0 && time.Now().Unix()-int64(iat.(float64)) > plugin.freshness {
330+
if plugin.allowRefresh(claims) {
332331
return http.StatusUnauthorized, err
333332
} else {
334333
return http.StatusForbidden, err
@@ -342,6 +341,20 @@ func (plugin *JWTPlugin) validate(request *http.Request, variables *TemplateVari
342341
return http.StatusOK, nil
343342
}
344343

344+
// allowRefresh returns true if freshness window is configured and the token has an iat claim that is older than the freshness window.
345+
func (plugin *JWTPlugin) allowRefresh(claims jwt.MapClaims) bool {
346+
if plugin.freshness == 0 {
347+
return false
348+
}
349+
iat, ok := claims["iat"]
350+
if !ok {
351+
return false
352+
}
353+
354+
value, err := iat.(json.Number).Int64()
355+
return err == nil && time.Now().Unix()-value > plugin.freshness
356+
}
357+
345358
// mapClaimsToHeaders maps any claims to headers as specified in the headerMap configuration.
346359
func (plugin *JWTPlugin) mapClaimsToHeaders(claims jwt.MapClaims, request *http.Request) {
347360
for header, claim := range plugin.headerMap {
@@ -387,6 +400,19 @@ func (requirement ValueRequirement) Validate(value any, variables *TemplateVaria
387400
return false
388401
}
389402
return fnmatch.Match(value, required, 0) || value == fmt.Sprintf("*.%s", required)
403+
404+
case json.Number:
405+
switch requirement.value.(type) {
406+
case int:
407+
converted, err := value.Int64()
408+
return err == nil && converted == int64(requirement.value.(int))
409+
case float64:
410+
converted, err := value.Float64()
411+
return err == nil && converted == requirement.value.(float64)
412+
default:
413+
log.Printf("unsupported requirement type for json.Number comparison: %T %v", requirement.value, requirement.value)
414+
return false
415+
}
390416
}
391417

392418
return reflect.DeepEqual(value, requirement.value)

jwt_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func TestServeHTTP(tester *testing.T) {
245245
HeaderName: "Authorization",
246246
},
247247
{
248-
Name: "StatusUnauthorized when within window of freshness",
248+
Name: "StatusUnauthorized when outside window of freshness",
249249
Expect: http.StatusUnauthorized,
250250
Config: `
251251
secret: fixed secret
@@ -255,6 +255,18 @@ func TestServeHTTP(tester *testing.T) {
255255
Method: jwt.SigningMethodHS256,
256256
HeaderName: "Authorization",
257257
},
258+
{
259+
Name: "StatusForbidden when no window of freshness",
260+
Expect: http.StatusForbidden,
261+
Config: `
262+
secret: fixed secret
263+
freshness: 0
264+
require:
265+
aud: test`,
266+
Claims: `{"aud": "other", "iat": 1692451139}`,
267+
Method: jwt.SigningMethodHS256,
268+
HeaderName: "Authorization",
269+
},
258270
{
259271
Name: "template requirement",
260272
Expect: http.StatusOK,
@@ -1313,6 +1325,39 @@ func TestServeHTTP(tester *testing.T) {
13131325
Method: jwt.SigningMethodES256,
13141326
CookieName: "Authorization",
13151327
},
1328+
{
1329+
Name: "large integer needing json.Number to keep precision",
1330+
Expect: http.StatusOK,
1331+
Config: `
1332+
infoToStdout: true
1333+
require:
1334+
large: 1147953659032899584`,
1335+
ClaimsMap: jwt.MapClaims{"large": 1147953659032899584},
1336+
Method: jwt.SigningMethodES256,
1337+
CookieName: "Authorization",
1338+
},
1339+
{
1340+
Name: "float claim",
1341+
Expect: http.StatusOK,
1342+
Config: `
1343+
infoToStdout: true
1344+
require:
1345+
float: 0.0`,
1346+
ClaimsMap: jwt.MapClaims{"float": 0.0},
1347+
Method: jwt.SigningMethodES256,
1348+
CookieName: "Authorization",
1349+
},
1350+
{
1351+
Name: "claim with different type",
1352+
Expect: http.StatusForbidden,
1353+
Config: `
1354+
infoToStdout: true
1355+
require:
1356+
large: "1147953659032899584"`,
1357+
ClaimsMap: jwt.MapClaims{"large": 1147953659032899584},
1358+
Method: jwt.SigningMethodES256,
1359+
CookieName: "Authorization",
1360+
},
13161361
}
13171362

13181363
for _, test := range tests {

0 commit comments

Comments
 (0)