diff --git a/apidef/api_definitions.go b/apidef/api_definitions.go index a3d91126f059..91a8848c7bfd 100644 --- a/apidef/api_definitions.go +++ b/apidef/api_definitions.go @@ -347,9 +347,9 @@ type APIDefinition struct { 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"` + JWTIssuedAtValidationSkew uint64 `bson:"jwt_issued_at_validation_skew" json:"jwt_issued_at_validation_skew"` + JWTExpiresAtValidationSkew uint64 `bson:"jwt_expires_at_validation_skew" json:"jwt_expires_at_validation_skew"` + JWTNotBeforeValidationSkew uint64 `bson:"jwt_not_before_validation_skew" json:"jwt_not_before_validation_skew"` JWTSkipKid bool `bson:"jwt_skip_kid" json:"jwt_skip_kid"` NotificationsDetails NotificationsManager `bson:"notifications" json:"notifications"` EnableSignatureChecking bool `bson:"enable_signature_checking" json:"enable_signature_checking"` diff --git a/mw_jwt.go b/mw_jwt.go index 212b3e14f8f5..6227313ade49 100644 --- a/mw_jwt.go +++ b/mw_jwt.go @@ -487,20 +487,18 @@ func (k *JWTMiddleware) timeValidateJWTClaims(c jwt.MapClaims) *jwt.ValidationEr 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") + if !c.VerifyExpiresAt(now-int64(k.Spec.JWTExpiresAtValidationSkew), false) { + vErr.Inner = errors.New("token has expired") vErr.Errors |= jwt.ValidationErrorExpired } - if !k.Spec.JWTDisableIssuedAtValidation && c.VerifyIssuedAt(now, false) == false { - vErr.Inner = fmt.Errorf("Token used before issued") + if c.VerifyIssuedAt(now+int64(k.Spec.JWTIssuedAtValidationSkew), false) == false { + vErr.Inner = errors.New("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") + if c.VerifyNotBefore(now+int64(k.Spec.JWTNotBeforeValidationSkew), false) == false { + vErr.Inner = errors.New("token is not valid yet") vErr.Errors |= jwt.ValidationErrorNotValidYet } diff --git a/mw_jwt_test.go b/mw_jwt_test.go index 113be99d4b5a..e9a7f137fe21 100644 --- a/mw_jwt_test.go +++ b/mw_jwt_test.go @@ -621,10 +621,21 @@ func TestJWTSessionRSAWithRawSourceInvalidPolicyID(t *testing.T) { }) } -func TestJWTSessionInvalidClaims(t *testing.T) { +func TestJWTSessionExpiresAtValidationConfigs(t *testing.T) { ts := newTykTestServer() defer ts.Close() + pID := createPolicy() + jwtAuthHeaderGen := func(skew time.Duration) map[string]string { + jwtToken := createJWKToken(func(t *jwt.Token) { + t.Claims.(jwt.MapClaims)["policy_id"] = pID + t.Claims.(jwt.MapClaims)["user_id"] = "user123" + t.Claims.(jwt.MapClaims)["exp"] = time.Now().Add(skew).Unix() + }) + + return map[string]string{"authorization": jwtToken} + } + spec := buildAPI(func(spec *APISpec) { spec.UseKeylessAccess = false spec.EnableJWT = true @@ -635,39 +646,209 @@ func TestJWTSessionInvalidClaims(t *testing.T) { spec.Proxy.ListenPath = "/" })[0] - pID := createPolicy() + // This test is successful by definition + t.Run("Expiry_After_now--Valid_jwt", func(t *testing.T) { + spec.JWTExpiresAtValidationSkew = 0 //Default value + loadAPI(spec) - t.Run("Fail if token expired", func(t *testing.T) { - spec.JWTDisableExpiresAtValidation = false + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Second), Code: http.StatusOK, + }) + }) + + // This test is successful by definition, so it's true also with skew, but just to avoid confusion. + t.Run("Expiry_After_now-Add_skew--Valid_jwt", func(t *testing.T) { + spec.JWTExpiresAtValidationSkew = 1 loadAPI(spec) + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Second), Code: http.StatusOK, + }) + }) + + t.Run("Expiry_Before_now--Invalid_jwt", func(t *testing.T) { + spec.JWTExpiresAtValidationSkew = 0 //Default value + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(-time.Second), + Code: http.StatusUnauthorized, + BodyMatch: "Key not authorized: token has expired", + }) + }) + + t.Run("Expired_token-Before_now-Huge_skew--Valid_jwt", func(t *testing.T) { + spec.JWTExpiresAtValidationSkew = 1000 // This value doesn't matter since validation is disabled + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(-time.Second), Code: http.StatusOK, + }) + }) + + t.Run("Expired_token-Before_now-Add_skew--Valid_jwt", func(t *testing.T) { + spec.JWTExpiresAtValidationSkew = 1 + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(-time.Second), Code: http.StatusOK, + }) + }) +} + +func TestJWTSessionIssueAtValidationConfigs(t *testing.T) { + ts := newTykTestServer() + defer ts.Close() + + pID := createPolicy() + jwtAuthHeaderGen := func(skew time.Duration) map[string]string { 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() + t.Claims.(jwt.MapClaims)["user_id"] = "user123" + t.Claims.(jwt.MapClaims)["iat"] = time.Now().Add(skew).Unix() + }) + + return map[string]string{"authorization": jwtToken} + } + + 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] + + // This test is successful by definition + t.Run("IssuedAt_Before_now-no_skew--Valid_jwt", func(t *testing.T) { + spec.JWTIssuedAtValidationSkew = 0 + + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(-time.Second), Code: http.StatusOK, }) - authHeaders := map[string]string{"authorization": jwtToken} + }) + + t.Run("Expiry_after_now--Invalid_jwt", func(t *testing.T) { + spec.JWTExpiresAtValidationSkew = 0 //Default value + + loadAPI(spec) ts.Run(t, test.TestCase{ - Headers: authHeaders, - Code: 401, - BodyMatch: "Key not authorized: Token is expired", + Headers: jwtAuthHeaderGen(-time.Second), Code: http.StatusOK, }) }) - t.Run("Pass if token expired and validation disabled", func(t *testing.T) { - spec.JWTDisableExpiresAtValidation = true + t.Run("IssueAt-After_now-no_skew--Invalid_jwt", func(t *testing.T) { + spec.JWTIssuedAtValidationSkew = 0 + loadAPI(spec) + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Minute), + Code: http.StatusUnauthorized, + BodyMatch: "Key not authorized: token used before issued", + }) + }) + + t.Run("IssueAt--After_now-Huge_skew--valid_jwt", func(t *testing.T) { + spec.JWTIssuedAtValidationSkew = 1000 // This value doesn't matter since validation is disabled + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Second), + Code: http.StatusOK, + }) + }) + + // True by definition + t.Run("IssueAt-Before_now-Add_skew--not_valid_jwt", func(t *testing.T) { + spec.JWTIssuedAtValidationSkew = 2 // 2 seconds + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(-3 * time.Second), Code: http.StatusOK, + }) + }) + + t.Run("IssueAt-After_now-Add_skew--Valid_jwt", func(t *testing.T) { + spec.JWTIssuedAtValidationSkew = 1 + + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Second), Code: http.StatusOK, + }) + }) +} + +func TestJWTSessionNotBeforeValidationConfigs(t *testing.T) { + ts := newTykTestServer() + defer ts.Close() + + pID := createPolicy() + jwtAuthHeaderGen := func(skew time.Duration) map[string]string { 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() + t.Claims.(jwt.MapClaims)["user_id"] = "user123" + t.Claims.(jwt.MapClaims)["nbf"] = time.Now().Add(skew).Unix() + }) + return map[string]string{"authorization": jwtToken} + } + + spec := buildAPI(func(spec *APISpec) { + spec.UseKeylessAccess = false + spec.EnableJWT = true + spec.Proxy.ListenPath = "/" + spec.JWTSigningMethod = "rsa" + spec.JWTSource = base64.StdEncoding.EncodeToString([]byte(jwtRSAPubKey)) + spec.JWTIdentityBaseField = "user_id" + spec.JWTPolicyFieldName = "policy_id" + })[0] + + // This test is successful by definition + t.Run("NotBefore_Before_now-Valid_jwt", func(t *testing.T) { + spec.JWTNotBeforeValidationSkew = 0 + + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(-time.Second), Code: http.StatusOK, }) - authHeaders := map[string]string{"authorization": jwtToken} + }) + + t.Run("NotBefore_After_now--Invalid_jwt", func(t *testing.T) { + spec.JWTNotBeforeValidationSkew = 0 //Default value + + loadAPI(spec) ts.Run(t, test.TestCase{ - Headers: authHeaders, Code: http.StatusOK, + Headers: jwtAuthHeaderGen(+time.Second), + Code: http.StatusUnauthorized, + BodyMatch: "Key not authorized: token is not valid yet", + }) + }) + + t.Run("NotBefore_After_now-Add_skew--valid_jwt", func(t *testing.T) { + spec.JWTNotBeforeValidationSkew = 1 + + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Second), Code: http.StatusOK, + }) + }) + + t.Run("NotBefore_After_now-Huge_skew--valid_jwt", func(t *testing.T) { + spec.JWTNotBeforeValidationSkew = 1000 // This value is so high that it's actually similar to disabling the claim. + + loadAPI(spec) + + ts.Run(t, test.TestCase{ + Headers: jwtAuthHeaderGen(+time.Second), Code: http.StatusOK, }) }) } @@ -930,7 +1111,8 @@ func BenchmarkJWTSessionRSAWithEncodedJWK(b *testing.B) { ts.Run( b, test.TestCase{ - Headers: authHeaders, Code: http.StatusOK, + Headers: authHeaders, + Code: http.StatusOK, }, ) }