Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ experimental:
plugins:
jwt:
moduleName: github.com/agilezebra/jwt-middleware
version: v1.2.18
version: v1.2.19
```
1b. or with command-line options:

```yaml
command:
...
- "--experimental.plugins.jwt.modulename=github.com/agilezebra/jwt-middleware"
- "--experimental.plugins.jwt.version=v1.2.18"
- "--experimental.plugins.jwt.version=v1.2.19"
```

2) Configure and activate the plugin as a middleware in your dynamic traefik config:
Expand Down
32 changes: 29 additions & 3 deletions jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
plugin := JWTPlugin{
next: next,
name: name,
parser: jwt.NewParser(jwt.WithValidMethods(config.ValidMethods)),
parser: jwt.NewParser(jwt.WithValidMethods(config.ValidMethods), jwt.WithJSONNumber()),
secret: key,
issuers: canonicalizeDomains(config.Issuers),
clients: createClients(config.InsecureSkipVerify),
Expand Down Expand Up @@ -327,8 +327,7 @@ func (plugin *JWTPlugin) validate(request *http.Request, variables *TemplateVari
if !plugin.validateClaim(claim, claims, requirements, variables) {
err := fmt.Errorf("claim is not valid: %s", claim)
// If the token is older than our freshness window, we allow that reauthorization might fix it
iat, ok := claims["iat"]
if ok && plugin.freshness != 0 && time.Now().Unix()-int64(iat.(float64)) > plugin.freshness {
if plugin.allowRefresh(claims) {
return http.StatusUnauthorized, err
} else {
return http.StatusForbidden, err
Expand All @@ -342,6 +341,20 @@ func (plugin *JWTPlugin) validate(request *http.Request, variables *TemplateVari
return http.StatusOK, nil
}

// allowRefresh returns true if freshness window is configured and the token has an iat claim that is older than the freshness window.
func (plugin *JWTPlugin) allowRefresh(claims jwt.MapClaims) bool {
if plugin.freshness == 0 {
return false
}
iat, ok := claims["iat"]
if !ok {
return false
}

value, err := iat.(json.Number).Int64()
return err == nil && time.Now().Unix()-value > plugin.freshness
}

// mapClaimsToHeaders maps any claims to headers as specified in the headerMap configuration.
func (plugin *JWTPlugin) mapClaimsToHeaders(claims jwt.MapClaims, request *http.Request) {
for header, claim := range plugin.headerMap {
Expand Down Expand Up @@ -387,6 +400,19 @@ func (requirement ValueRequirement) Validate(value any, variables *TemplateVaria
return false
}
return fnmatch.Match(value, required, 0) || value == fmt.Sprintf("*.%s", required)

case json.Number:
switch requirement.value.(type) {
case int:
converted, err := value.Int64()
return err == nil && converted == int64(requirement.value.(int))
case float64:
converted, err := value.Float64()
return err == nil && converted == requirement.value.(float64)
default:
log.Printf("unsupported requirement type for json.Number comparison: %T %v", requirement.value, requirement.value)
return false
}
}

return reflect.DeepEqual(value, requirement.value)
Expand Down
47 changes: 46 additions & 1 deletion jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ func TestServeHTTP(tester *testing.T) {
HeaderName: "Authorization",
},
{
Name: "StatusUnauthorized when within window of freshness",
Name: "StatusUnauthorized when outside window of freshness",
Expect: http.StatusUnauthorized,
Config: `
secret: fixed secret
Expand All @@ -255,6 +255,18 @@ func TestServeHTTP(tester *testing.T) {
Method: jwt.SigningMethodHS256,
HeaderName: "Authorization",
},
{
Name: "StatusForbidden when no window of freshness",
Expect: http.StatusForbidden,
Config: `
secret: fixed secret
freshness: 0
require:
aud: test`,
Claims: `{"aud": "other", "iat": 1692451139}`,
Method: jwt.SigningMethodHS256,
HeaderName: "Authorization",
},
{
Name: "template requirement",
Expect: http.StatusOK,
Expand Down Expand Up @@ -1313,6 +1325,39 @@ func TestServeHTTP(tester *testing.T) {
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "large integer needing json.Number to keep precision",
Expect: http.StatusOK,
Config: `
infoToStdout: true
require:
large: 1147953659032899584`,
ClaimsMap: jwt.MapClaims{"large": 1147953659032899584},
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "float claim",
Expect: http.StatusOK,
Config: `
infoToStdout: true
require:
float: 0.0`,
ClaimsMap: jwt.MapClaims{"float": 0.0},
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "claim with different type",
Expect: http.StatusForbidden,
Config: `
infoToStdout: true
require:
large: "1147953659032899584"`,
ClaimsMap: jwt.MapClaims{"large": 1147953659032899584},
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
}

for _, test := range tests {
Expand Down