Skip to content

Commit

Permalink
Added way to control JWT validation
Browse files Browse the repository at this point in the history
Added new API definition fields for granular JWT validation:
- `jwt_disable_issued_at_validation`
- `jwt_disable_expires_at_validation`
- `jwt_disable_not_before_validation`
  • Loading branch information
buger committed May 4, 2018
1 parent 87fb264 commit abb1b35
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 22 deletions.
Binary file removed .DS_Store
Binary file not shown.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
.idea/
.vagrant/
.vscode/
Expand Down
41 changes: 22 additions & 19 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,25 +326,28 @@ type APIDefinition struct {
AllowedAuthorizeTypes []osin.AuthorizeRequestType `bson:"allowed_authorize_types" json:"allowed_authorize_types"`
AuthorizeLoginRedirect string `bson:"auth_login_redirect" json:"auth_login_redirect"`
} `bson:"oauth_meta" json:"oauth_meta"`
Auth Auth `bson:"auth" json:"auth"`
UseBasicAuth bool `bson:"use_basic_auth" json:"use_basic_auth"`
UseMutualTLSAuth bool `bson:"use_mutual_tls_auth" json:"use_mutual_tls_auth"`
ClientCertificates []string `bson:"client_certificates" json:"client_certificates"`
UpstreamCertificates map[string]string `bson:"upstream_certificates" json:"upstream_certificates"`
PinnedPublicKeys map[string]string `bson:"pinned_public_keys" json:"pinned_public_keys"`
EnableJWT bool `bson:"enable_jwt" json:"enable_jwt"`
UseStandardAuth bool `bson:"use_standard_auth" json:"use_standard_auth"`
EnableCoProcessAuth bool `bson:"enable_coprocess_auth" json:"enable_coprocess_auth"`
JWTSigningMethod string `bson:"jwt_signing_method" json:"jwt_signing_method"`
JWTSource string `bson:"jwt_source" json:"jwt_source"`
JWTIdentityBaseField string `bson:"jwt_identit_base_field" json:"jwt_identity_base_field"`
JWTClientIDBaseField string `bson:"jwt_client_base_field" json:"jwt_client_base_field"`
JWTPolicyFieldName string `bson:"jwt_policy_field_name" json:"jwt_policy_field_name"`
NotificationsDetails NotificationsManager `bson:"notifications" json:"notifications"`
EnableSignatureChecking bool `bson:"enable_signature_checking" json:"enable_signature_checking"`
HmacAllowedClockSkew float64 `bson:"hmac_allowed_clock_skew" json:"hmac_allowed_clock_skew"`
BaseIdentityProvidedBy AuthTypeEnum `bson:"base_identity_provided_by" json:"base_identity_provided_by"`
VersionDefinition struct {
Auth Auth `bson:"auth" json:"auth"`
UseBasicAuth bool `bson:"use_basic_auth" json:"use_basic_auth"`
UseMutualTLSAuth bool `bson:"use_mutual_tls_auth" json:"use_mutual_tls_auth"`
ClientCertificates []string `bson:"client_certificates" json:"client_certificates"`
UpstreamCertificates map[string]string `bson:"upstream_certificates" json:"upstream_certificates"`
PinnedPublicKeys map[string]string `bson:"pinned_public_keys" json:"pinned_public_keys"`
EnableJWT bool `bson:"enable_jwt" json:"enable_jwt"`
UseStandardAuth bool `bson:"use_standard_auth" json:"use_standard_auth"`
EnableCoProcessAuth bool `bson:"enable_coprocess_auth" json:"enable_coprocess_auth"`
JWTSigningMethod string `bson:"jwt_signing_method" json:"jwt_signing_method"`
JWTSource string `bson:"jwt_source" json:"jwt_source"`
JWTIdentityBaseField string `bson:"jwt_identit_base_field" json:"jwt_identity_base_field"`
JWTClientIDBaseField string `bson:"jwt_client_base_field" json:"jwt_client_base_field"`
JWTPolicyFieldName string `bson:"jwt_policy_field_name" json:"jwt_policy_field_name"`
JWTDisableIssuedAtValidation bool `bson:"jwt_disable_issued_at_validation" json:"jwt_disable_issued_at_validation"`
JWTDisableExpiresAtValidation bool `bson:"jwt_disable_expires_at_validation" json:"jwt_disable_expires_at_validation"`
JWTDisableNotBeforeValidation bool `bson:"jwt_disable_not_before_validation" json:"jwt_disable_not_before_validation"`
NotificationsDetails NotificationsManager `bson:"notifications" json:"notifications"`
EnableSignatureChecking bool `bson:"enable_signature_checking" json:"enable_signature_checking"`
HmacAllowedClockSkew float64 `bson:"hmac_allowed_clock_skew" json:"hmac_allowed_clock_skew"`
BaseIdentityProvidedBy AuthTypeEnum `bson:"base_identity_provided_by" json:"base_identity_provided_by"`
VersionDefinition struct {
Location string `bson:"location" json:"location"`
Key string `bson:"key" json:"key"`
} `bson:"definition" json:"definition"`
Expand Down
43 changes: 40 additions & 3 deletions mw_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,8 +390,11 @@ func (k *JWTMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _
// enable bearer token format
rawJWT = stripBearer(rawJWT)

// Use own validation logic, see below
parser := &jwt.Parser{SkipClaimsValidation: true}

// Verify the token
token, err := jwt.Parse(rawJWT, func(token *jwt.Token) (interface{}, error) {
token, err := parser.Parse(rawJWT, func(token *jwt.Token) (interface{}, error) {
// Don't forget to validate the alg is what you expect:
switch k.Spec.JWTSigningMethod {
case "hmac":
Expand Down Expand Up @@ -432,6 +435,10 @@ func (k *JWTMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _
})

if err == nil && token.Valid {
if jwtErr := k.validateJWTClaims(token.Claims.(jwt.MapClaims)); jwtErr != nil {
return errors.New("Key not authorized: " + jwtErr.Error()), 401
}

// Token is valid - let's move on

// Are we mapping to a central JWT Secret?
Expand All @@ -445,12 +452,42 @@ func (k *JWTMiddleware) ProcessRequest(w http.ResponseWriter, r *http.Request, _
logEntry := getLogEntryForRequest(r, "", nil)
logEntry.Info("Attempted JWT access with non-existent key.")

k.reportLoginFailure(tykId, r)

if err != nil {
logEntry.Error("JWT validation error: ", err)
return errors.New("Key not authorized:" + err.Error()), 403
} else {
return errors.New("Key not authorized"), 403
}
}

k.reportLoginFailure(tykId, r)
return errors.New("Key not authorized"), 403
func (k *JWTMiddleware) validateJWTClaims(c jwt.MapClaims) *jwt.ValidationError {
vErr := new(jwt.ValidationError)
now := time.Now().Unix()

// The claims below are optional, by default, so if they are set to the
// default value in Go, let's not fail the verification for them.
if !k.Spec.JWTDisableExpiresAtValidation && c.VerifyExpiresAt(now, false) == false {
vErr.Inner = errors.New("Token is expired")
vErr.Errors |= jwt.ValidationErrorExpired
}

if !k.Spec.JWTDisableIssuedAtValidation && c.VerifyIssuedAt(now, false) == false {
vErr.Inner = fmt.Errorf("Token used before issued")
vErr.Errors |= jwt.ValidationErrorIssuedAt
}

if !k.Spec.JWTDisableNotBeforeValidation && c.VerifyNotBefore(now, false) == false {
vErr.Inner = fmt.Errorf("token is not valid yet")
vErr.Errors |= jwt.ValidationErrorNotValidYet
}

if vErr.Errors == 0 {
return nil
}

return vErr
}

func ctxSetJWTContextVars(s *APISpec, r *http.Request, token *jwt.Token) {
Expand Down
49 changes: 49 additions & 0 deletions mw_jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,55 @@ func TestJWTSessionRSAWithRawSourceInvalidPolicyID(t *testing.T) {
})
}

func TestJWTSessionInvalidClaims(t *testing.T) {
ts := newTykTestServer()
defer ts.Close()

spec := buildAPI(func(spec *APISpec) {
spec.UseKeylessAccess = false
spec.EnableJWT = true
spec.JWTSigningMethod = "rsa"
spec.JWTSource = base64.StdEncoding.EncodeToString([]byte(jwtRSAPubKey))
spec.JWTIdentityBaseField = "user_id"
spec.JWTPolicyFieldName = "policy_id"
spec.Proxy.ListenPath = "/"
})[0]

pID := createPolicy()

t.Run("Fail if token expired", func(t *testing.T) {
spec.JWTDisableExpiresAtValidation = false
loadAPI(spec)

jwtToken := createJWKToken(func(t *jwt.Token) {
t.Claims.(jwt.MapClaims)["policy_id"] = pID
t.Claims.(jwt.MapClaims)["user_id"] = "user"
t.Claims.(jwt.MapClaims)["exp"] = time.Now().Add(-time.Hour * 72).Unix()
})
authHeaders := map[string]string{"authorization": jwtToken}

ts.Run(t, test.TestCase{
Headers: authHeaders, Code: 401, BodyMatch: "Key not authorized: Token is expired",
})
})

t.Run("Pass if token expired and validation disabled", func(t *testing.T) {
spec.JWTDisableExpiresAtValidation = true
loadAPI(spec)

jwtToken := createJWKToken(func(t *jwt.Token) {
t.Claims.(jwt.MapClaims)["policy_id"] = pID
t.Claims.(jwt.MapClaims)["user_id"] = "user"
t.Claims.(jwt.MapClaims)["exp"] = time.Now().Add(-time.Hour * 72).Unix()
})
authHeaders := map[string]string{"authorization": jwtToken}

ts.Run(t, test.TestCase{
Headers: authHeaders, Code: 200,
})
})
}

func TestJWTExistingSessionRSAWithRawSourceInvalidPolicyID(t *testing.T) {
ts := newTykTestServer()
defer ts.Close()
Expand Down

1 comment on commit abb1b35

@letzya
Copy link
Contributor

@letzya letzya commented on abb1b35 May 8, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix #1670

Please sign in to comment.