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.3.2
version: v1.3.3
```
1b. or with command-line options:

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

2) Configure and activate the plugin as a middleware in your dynamic traefik config:
Expand Down
7 changes: 7 additions & 0 deletions jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ type Config struct {
RemoveMissingHeaders bool `json:"removeMissingHeaders,omitempty"`
ForwardToken bool `json:"forwardToken,omitempty"`
Freshness int64 `json:"freshness,omitempty"`
LogUnauthorized string `json:"logUnauthorized,omitempty"`
}

// JWTPlugin is a traefik middleware plugin that authorizes access based on JWT tokens.
Expand All @@ -70,6 +71,7 @@ type JWTPlugin struct {
forwardToken bool // If true, the token is forwarded to the backend
freshness int64 // The maximum age of a token in seconds
environment map[string]string // Map of environment variables
logUnauthorized string // If set, log the details of the failed requirements to the level specified
}

// TemplateVariables are the per-request variables passed to Go templates for interpolation, such as the require and redirect templates.
Expand Down Expand Up @@ -161,6 +163,7 @@ func New(_ context.Context, next http.Handler, config *Config, name string) (htt
removeMissingHeaders: config.RemoveMissingHeaders,
forwardToken: config.ForwardToken,
freshness: config.Freshness,
logUnauthorized: strings.ToUpper(config.LogUnauthorized),
environment: environment(),
}

Expand Down Expand Up @@ -610,6 +613,10 @@ func (plugin *JWTPlugin) NewTemplateVariables(request *http.Request) *TemplateVa
variables["URL"] = fmt.Sprintf("%s://%s%s", variables["Scheme"], variables["Host"], variables["Path"])
}

if plugin.logUnauthorized != "" {
variables["logUnauthorized"] = plugin.logUnauthorized
}

return &variables
}

Expand Down
25 changes: 25 additions & 0 deletions jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,7 @@ func TestServeHTTP(tester *testing.T) {
Expect: http.StatusForbidden,
Config: `
secret: fixed secret
logUnauthorized: info
require:
authority: "test.company.com"`,
Claims: `{"authority": "*.example.com"}`,
Expand Down Expand Up @@ -1367,6 +1368,18 @@ func TestServeHTTP(tester *testing.T) {
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "invalid integer claim",
Expect: http.StatusForbidden,
Config: `
infoToStdout: true
logUnauthorized: info
require:
int: 0`,
ClaimsMap: jwt.MapClaims{"int": 1},
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "float claim",
Expect: http.StatusOK,
Expand All @@ -1378,6 +1391,18 @@ func TestServeHTTP(tester *testing.T) {
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "invalid float claim",
Expect: http.StatusForbidden,
Config: `
infoToStdout: true
logUnauthorized: info
require:
float: 0.0`,
ClaimsMap: jwt.MapClaims{"float": 1.0},
Method: jwt.SigningMethodES256,
CookieName: "Authorization",
},
{
Name: "claim with different type",
Expect: http.StatusForbidden,
Expand Down
19 changes: 16 additions & 3 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"log"
"strings"

"github.com/agilezebra/jwt-middleware/logger"
"github.com/danwakefield/fnmatch"
)

Expand Down Expand Up @@ -122,6 +123,7 @@ outer:
// variables is required in the interface and passed on recursively but ultimately ignored by ValueRequirement
// having been already interpolated by TemplateRequirement
func (requirement ValueRequirement) Validate(value any, variables *TemplateVariables) error {
level, verbose := (*variables)["logUnauthorized"]
switch value := value.(type) {
case []any:
for _, value := range value {
Expand All @@ -142,22 +144,33 @@ func (requirement ValueRequirement) Validate(value any, variables *TemplateVaria
case string:
required, ok := requirement.value.(string)
if ok {
if fnmatch.Match(value, required, 0) || value == fmt.Sprintf("*.%s", required) {
if wildcardMatch(value, required) {
return nil
}
if verbose {
logger.Log(level, "claim is not valid: require:%s got:%v", required, value)
}
}
case json.Number:
switch requirement.value.(type) {
case int:
converted, err := value.Int64()
if err == nil && converted == int64(requirement.value.(int)) {
required := int64(requirement.value.(int))
if err == nil && converted == required {
return nil
}
if verbose {
logger.Log(level, "claim is not valid: require:%d got:%v", required, value)
}
case float64:
converted, err := value.Float64()
if err == nil && converted == requirement.value.(float64) {
required := requirement.value.(float64)
if err == nil && converted == required {
return nil
}
if verbose {
logger.Log(level, "claim is not valid: require:%f got:%v", required, value)
}
default:
log.Printf("unsupported requirement type for json.Number comparison: %T %v", requirement.value, requirement.value)
return fmt.Errorf("unsupported requirement type for json.Number comparison")
Expand Down
Loading