Skip to content

Commit d6a9aa5

Browse files
authored
[TT-15359]: Added extra jwt validation (#7269)
<details open> <summary><a href="https://tyktech.atlassian.net/browse/TT-15359" title="TT-15359" target="_blank">TT-15359</a></summary> <br /> <table> <tr> <th>Summary</th> <td> Core Registered Claims Validation</td> </tr> <tr> <th>Type</th> <td> <img alt="Story" src="https://tyktech.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium" /> Story </td> </tr> <tr> <th>Status</th> <td>In Dev</td> </tr> <tr> <th>Points</th> <td>N/A</td> </tr> <tr> <th>Labels</th> <td><a href="https://tyktech.atlassian.net/issues?jql=project%20%3D%20TT%20AND%20labels%20%3D%20jira_escalated%20ORDER%20BY%20created%20DESC" title="jira_escalated">jira_escalated</a></td> </tr> </table> </details> <!-- do not remove this marker as it will break jira-lint's functionality. added_by_jira_lint --> --- <!-- Provide a general summary of your changes in the Title above --> ## Description [TT-15359](https://tyktech.atlassian.net/browse/TT-15359) <!-- Describe your changes in detail --> ## Related Issue <!-- This project only accepts pull requests related to open issues. --> <!-- If suggesting a new feature or change, please discuss it in an issue first. --> <!-- If fixing a bug, there should be an issue describing it with steps to reproduce. --> <!-- OSS: Please link to the issue here. Tyk: please create/link the JIRA ticket. --> ## Motivation and Context <!-- Why is this change required? What problem does it solve? --> ## How This Has Been Tested <!-- Please describe in detail how you tested your changes --> <!-- Include details of your testing environment, and the tests --> <!-- you ran to see how your change affects other areas of the code, etc. --> <!-- This information is helpful for reviewers and QA. --> ## Screenshots (if appropriate) ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Refactoring or add test (improvements in base code or adds test coverage to functionality) ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply --> <!-- If there are no documentation updates required, mark the item as checked. --> <!-- Raise up any additional concerns not covered by the checklist. --> - [ ] I ensured that the documentation is up to date - [ ] I explained why this PR updates go.mod in detail with reasoning why it's required - [ ] I would like a code coverage CI quality gate exception and have explained why [TT-15359]: https://tyktech.atlassian.net/browse/TT-15359?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 35b0c98 commit d6a9aa5

File tree

9 files changed

+1068
-63
lines changed

9 files changed

+1068
-63
lines changed

apidef/oas/authentication.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,11 @@ type Scopes struct {
484484
// - For JWT: `scopes.jwt.scope_claim_name`
485485
ClaimName string `bson:"claimName,omitempty" json:"claimName,omitempty"`
486486

487+
// Claims contains a list of claims that contains the claim name.
488+
// The first match from the list of claims in the token is used.
489+
// OAS only field applied to OAS apis.
490+
Claims []string `bson:"claims,omitempty" json:"claims,omitempty"`
491+
487492
// ScopeToPolicyMapping contains the mappings of scopes to policy IDs.
488493
//
489494
// Tyk classic API definition:
@@ -495,6 +500,9 @@ type Scopes struct {
495500
// Fill fills *Scopes from *apidef.ScopeClaim.
496501
func (s *Scopes) Fill(scopeClaim *apidef.ScopeClaim) {
497502
s.ClaimName = scopeClaim.ScopeClaimName
503+
if s.ClaimName != "" {
504+
s.Claims = []string{scopeClaim.ScopeClaimName}
505+
}
498506

499507
s.ScopeToPolicyMapping = []ScopeToPolicy{}
500508

apidef/oas/authentication_test.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,41 @@ func TestAuthentication(t *testing.T) {
2222
}
2323

2424
func TestScopes(t *testing.T) {
25-
var emptyScopes Scopes
25+
t.Run("default", func(t *testing.T) {
26+
var emptyScopes Scopes
2627

27-
scopeClaim := apidef.ScopeClaim{}
28-
emptyScopes.ExtractTo(&scopeClaim)
28+
scopeClaim := apidef.ScopeClaim{}
29+
emptyScopes.ExtractTo(&scopeClaim)
2930

30-
var resultScopes Scopes
31-
resultScopes.Fill(&scopeClaim)
31+
var resultScopes Scopes
32+
resultScopes.Fill(&scopeClaim)
33+
34+
assert.Equal(t, emptyScopes, resultScopes)
35+
})
36+
t.Run("fill scope claim", func(t *testing.T) {
37+
var emptyScopes Scopes
38+
39+
scopeClaim := apidef.ScopeClaim{
40+
ScopeClaimName: "test",
41+
}
42+
43+
emptyScopes.Fill(&scopeClaim)
44+
45+
assert.Equal(t, emptyScopes.Claims, []string{scopeClaim.ScopeClaimName})
46+
})
47+
48+
t.Run("extract scope claim", func(t *testing.T) {
49+
var emptydefScopeClaim apidef.ScopeClaim
50+
51+
scope := Scopes{
52+
Claims: []string{"test", "second"},
53+
ClaimName: "test",
54+
}
55+
56+
scope.ExtractTo(&emptydefScopeClaim)
57+
assert.Equal(t, emptydefScopeClaim.ScopeClaimName, "test")
58+
})
3259

33-
assert.Equal(t, emptyScopes, resultScopes)
3460
}
3561

3662
func TestAuthSources(t *testing.T) {

apidef/oas/schema/x-tyk-api-gateway.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@
218218
"claimName": {
219219
"type": "string"
220220
},
221+
"claims": {
222+
"type": "array",
223+
"items": {"type": "string"}
224+
},
221225
"scopeToPolicyMapping": {
222226
"type": "array",
223227
"items": [
@@ -1612,12 +1616,36 @@
16121616
"identityBaseField": {
16131617
"type": "string"
16141618
},
1619+
"subjectClaims": {
1620+
"type": "array",
1621+
"items": {"type": "string"}
1622+
},
16151623
"skipKid": {
16161624
"type": "boolean"
16171625
},
16181626
"policyFieldName": {
16191627
"type": "string"
16201628
},
1629+
"basePolicyClaims": {
1630+
"type": "array",
1631+
"items": {"type": "string"}
1632+
},
1633+
"allowedIssuers": {
1634+
"type": "array",
1635+
"items": {"type": "string"}
1636+
},
1637+
"allowedAudiences": {
1638+
"type": "array",
1639+
"items": {"type": "string"}
1640+
},
1641+
"jtiValidation": {
1642+
"type": "array",
1643+
"items": {"type": "string"}
1644+
},
1645+
"allowedSubjects": {
1646+
"type": "array",
1647+
"items": {"type": "string"}
1648+
},
16211649
"clientBaseField": {
16221650
"type": "string"
16231651
},

apidef/oas/schema/x-tyk-api-gateway.strict.json

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,12 @@
229229
"claimName": {
230230
"type": "string"
231231
},
232+
"claims": {
233+
"type": "array",
234+
"items": {
235+
"type": "string"
236+
}
237+
},
232238
"scopeToPolicyMapping": {
233239
"type": "array",
234240
"items": [
@@ -1678,12 +1684,48 @@
16781684
"identityBaseField": {
16791685
"type": "string"
16801686
},
1687+
"subjectClaims": {
1688+
"type": "array",
1689+
"items": {
1690+
"type": "string"
1691+
}
1692+
},
16811693
"skipKid": {
16821694
"type": "boolean"
16831695
},
16841696
"policyFieldName": {
16851697
"type": "string"
16861698
},
1699+
"basePolicyClaims": {
1700+
"type": "array",
1701+
"items": {
1702+
"type": "string"
1703+
}
1704+
},
1705+
"allowedIssuers": {
1706+
"type": "array",
1707+
"items": {
1708+
"type": "string"
1709+
}
1710+
},
1711+
"allowedAudiences": {
1712+
"type": "array",
1713+
"items": {
1714+
"type": "string"
1715+
}
1716+
},
1717+
"jtiValidation": {
1718+
"type": "array",
1719+
"items": {
1720+
"type": "string"
1721+
}
1722+
},
1723+
"allowedSubjects": {
1724+
"type": "array",
1725+
"items": {
1726+
"type": "string"
1727+
}
1728+
},
16871729
"clientBaseField": {
16881730
"type": "string"
16891731
},

apidef/oas/security.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,10 @@ type JWT struct {
122122
// Tyk classic API definition: `jwt_identity_base_field`
123123
IdentityBaseField string `bson:"identityBaseField,omitempty" json:"identityBaseField,omitempty"`
124124

125+
// SubjectClaims specifies a list of claims that can be used to identity the subject of the JWT.
126+
// The field is an OAS only field and is only used in OAS APIs.
127+
SubjectClaims []string `bson:"subjectClaims,omitempty" json:"subjectClaims,omitempty"`
128+
125129
// SkipKid controls skipping using the `kid` claim from a JWT (default behaviour).
126130
// When this is true, the field configured in IdentityBaseField is checked first.
127131
//
@@ -134,6 +138,11 @@ type JWT struct {
134138
// Tyk classic API definition: `jwt_policy_field_name`
135139
PolicyFieldName string `bson:"policyFieldName,omitempty" json:"policyFieldName,omitempty"`
136140

141+
// BasePolicyClaims specifies a list of claims from which the base PolicyID is extracted.
142+
// The policy is applied to the session as a base policy.
143+
// The field is an OAS only field and is only used in OAS APIs.
144+
BasePolicyClaims []string `bson:"basePolicyClaims,omitempty" json:"basePolicyClaims,omitempty"`
145+
137146
// ClientBaseField is used when PolicyFieldName is not provided. It will get
138147
// a session key and use the policies from that. The field ensures that requests
139148
// use the same session.
@@ -164,6 +173,21 @@ type JWT struct {
164173
// Tyk classic API definition: `jwt_expires_at_validation_skew`.
165174
ExpiresAtValidationSkew uint64 `bson:"expiresAtValidationSkew,omitempty" json:"expiresAtValidationSkew,omitempty"`
166175

176+
// AllowedIssuers contains a list of accepted issuers for JWT validation.
177+
// When configured, the JWT's issuer claim must match one of these values.
178+
AllowedIssuers []string `bson:"allowedIssuers,omitempty" json:"allowedIssuers,omitempty"`
179+
180+
// AllowedAudiences contains a list of accepted audiences for JWT validation.
181+
// When configured, the JWT's audience claim must match one of these values.
182+
AllowedAudiences []string `bson:"allowedAudiences,omitempty" json:"allowedAudiences,omitempty"`
183+
184+
// JTIValidation contains the configuration for the validation of the JWT ID.
185+
JTIValidation JTIValidation `bson:"jtiValidation,omitempty" json:"jtiValidation,omitempty"`
186+
187+
// AllowedSubjects contains a list of accepted subjects for JWT validation.
188+
// When configured, the subject from kid/identityBaseField/sub must match one of these values.
189+
AllowedSubjects []string `bson:"allowedSubjects,omitempty" json:"allowedSubjects,omitempty"`
190+
167191
// IDPClientIDMappingDisabled prevents Tyk from automatically detecting the use of certain IDPs based on standard claims
168192
// that they include in the JWT: `client_id`, `cid`, `clientId`. Setting this flag to `true` disables the mapping and avoids
169193
// accidentally misidentifying the use of one of these IDPs if one of their standard values is configured in your JWT.
@@ -172,6 +196,13 @@ type JWT struct {
172196
IDPClientIDMappingDisabled bool `bson:"idpClientIdMappingDisabled,omitempty" json:"idpClientIdMappingDisabled,omitempty"`
173197
}
174198

199+
// JTIValidation contains the configuration for the validation of the JWT ID.
200+
type JTIValidation struct {
201+
// Enabled indicates whether JWT ID claim is required.
202+
// When true, tokens must include a 'jti' claim.
203+
Enabled bool `bson:"enabled" json:"enabled"`
204+
}
205+
175206
// Import populates *JWT based on arguments.
176207
func (j *JWT) Import(enable bool) {
177208
j.Enabled = enable
@@ -212,8 +243,14 @@ func (s *OAS) fillJWT(api apidef.APIDefinition) {
212243
jwt.JwksURIs = api.JWTJwksURIs
213244
jwt.SigningMethod = api.JWTSigningMethod
214245
jwt.IdentityBaseField = api.JWTIdentityBaseField
246+
if jwt.IdentityBaseField != "" {
247+
jwt.SubjectClaims = []string{jwt.IdentityBaseField}
248+
}
215249
jwt.SkipKid = api.JWTSkipKid
216250
jwt.PolicyFieldName = api.JWTPolicyFieldName
251+
if jwt.PolicyFieldName != "" {
252+
jwt.BasePolicyClaims = []string{api.JWTPolicyFieldName}
253+
}
217254
jwt.ClientBaseField = api.JWTClientIDBaseField
218255

219256
if jwt.Scopes == nil {
@@ -845,6 +882,18 @@ func (s *OAS) extractSecurityTo(api *apidef.APIDefinition) {
845882
}
846883
}
847884

885+
func (s *OAS) GetJWTConfiguration() *JWT {
886+
for keyName := range s.getTykSecuritySchemes() {
887+
if _, ok := s.Security[0][keyName]; ok {
888+
v := s.Components.SecuritySchemes[keyName].Value
889+
if v.Type == typeHTTP && v.Scheme == schemeBearer && v.BearerFormat == bearerFormatJWT {
890+
return s.getTykJWTAuth(keyName)
891+
}
892+
}
893+
}
894+
return nil
895+
}
896+
848897
func resetSecuritySchemes(api *apidef.APIDefinition) {
849898
api.AuthConfigs = nil
850899

apidef/oas/security_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,84 @@ import (
1010
"github.com/TykTechnologies/tyk/apidef"
1111
)
1212

13+
func TestGetJWTConfiguration(t *testing.T) {
14+
t.Run("should retrieve successfully", func(t *testing.T) {
15+
var api apidef.APIDefinition
16+
api.EnableJWT = true
17+
api.AuthConfigs = map[string]apidef.AuthConfig{
18+
apidef.JWTType: {
19+
Name: "jwtAuth",
20+
AuthHeaderName: "Authorization",
21+
},
22+
}
23+
24+
var oas OAS
25+
oas.SetTykExtension(&XTykAPIGateway{})
26+
oas.fillSecurity(api)
27+
28+
j := oas.GetTykExtension().Server.Authentication.SecuritySchemes["jwtAuth"].(*JWT)
29+
j.AllowedIssuers = []string{"issuer_one", "issuer_two"}
30+
j.AllowedAudiences = []string{"audience_one", "audience_two"}
31+
j.BasePolicyClaims = []string{"policy"}
32+
j.SubjectClaims = []string{"new_sub"}
33+
34+
oas.GetTykExtension().Server.Authentication.SecuritySchemes["jwtAuth"] = j
35+
gotten := oas.GetJWTConfiguration()
36+
37+
assert.Equal(t, j.AllowedIssuers, gotten.AllowedIssuers)
38+
assert.Equal(t, []string{"new_sub"}, gotten.SubjectClaims)
39+
assert.Equal(t, []string{"policy"}, gotten.BasePolicyClaims)
40+
assert.Equal(t, j.AllowedAudiences, gotten.AllowedAudiences)
41+
})
42+
43+
t.Run("should successfully convert identity and policy and return", func(t *testing.T) {
44+
var api apidef.APIDefinition
45+
api.EnableJWT = true
46+
api.AuthConfigs = map[string]apidef.AuthConfig{
47+
apidef.JWTType: {
48+
Name: "jwtAuth",
49+
AuthHeaderName: "Authorization",
50+
},
51+
}
52+
api.JWTIdentityBaseField = "new_sub"
53+
api.JWTPolicyFieldName = "policy"
54+
55+
var oas OAS
56+
oas.Fill(api)
57+
58+
j := oas.GetJWTConfiguration()
59+
assert.Equal(t, j.IdentityBaseField, "new_sub")
60+
assert.Equal(t, []string{"new_sub"}, j.SubjectClaims)
61+
assert.Equal(t, j.PolicyFieldName, "policy")
62+
assert.Equal(t, []string{"policy"}, j.BasePolicyClaims)
63+
64+
var newAPIDef apidef.APIDefinition
65+
oas.GetJWTConfiguration().PolicyFieldName = "policy"
66+
oas.GetJWTConfiguration().IdentityBaseField = "subject"
67+
oas.ExtractTo(&newAPIDef)
68+
69+
assert.Equal(t, "policy", newAPIDef.JWTPolicyFieldName)
70+
assert.Equal(t, "subject", newAPIDef.JWTIdentityBaseField)
71+
})
72+
73+
t.Run("should return nil", func(t *testing.T) {
74+
var auth apidef.AuthConfig
75+
Fill(t, &auth, 0)
76+
auth.DisableHeader = false
77+
78+
var api apidef.APIDefinition
79+
api.AuthConfigs = map[string]apidef.AuthConfig{
80+
apidef.AuthTokenType: auth,
81+
}
82+
83+
var oas OAS
84+
oas.SetTykExtension(&XTykAPIGateway{})
85+
oas.fillSecurity(api)
86+
87+
assert.Nil(t, oas.GetJWTConfiguration())
88+
})
89+
}
90+
1391
func TestOAS_Security(t *testing.T) {
1492
var auth apidef.AuthConfig
1593
Fill(t, &auth, 0)
@@ -342,6 +420,14 @@ func TestOAS_JWT(t *testing.T) {
342420
convertedOAS.SetTykExtension(&XTykAPIGateway{Server: Server{Authentication: &Authentication{SecuritySchemes: SecuritySchemes{}}}})
343421
convertedOAS.fillJWT(api)
344422

423+
// set OAS only fields since they will not be converted
424+
convertedOAS.GetJWTConfiguration().AllowedAudiences = oas.GetJWTConfiguration().AllowedAudiences
425+
convertedOAS.GetJWTConfiguration().AllowedIssuers = oas.GetJWTConfiguration().AllowedIssuers
426+
convertedOAS.GetJWTConfiguration().AllowedSubjects = oas.GetJWTConfiguration().AllowedSubjects
427+
convertedOAS.GetJWTConfiguration().SubjectClaims = oas.GetJWTConfiguration().SubjectClaims
428+
convertedOAS.GetJWTConfiguration().BasePolicyClaims = oas.GetJWTConfiguration().BasePolicyClaims
429+
convertedOAS.GetJWTConfiguration().Scopes.Claims = oas.GetJWTConfiguration().Scopes.Claims
430+
convertedOAS.GetJWTConfiguration().JTIValidation.Enabled = true
345431
assert.Equal(t, oas, convertedOAS)
346432
}
347433

@@ -568,6 +654,8 @@ func TestOAS_OIDC(t *testing.T) {
568654
convertedOAS.SetTykExtension(&XTykAPIGateway{Server: Server{Authentication: &Authentication{}}})
569655
convertedOAS.getTykAuthentication().Fill(api)
570656

657+
// set scope claims cause it is OAS only
658+
convertedOAS.Extensions[ExtensionTykAPIGateway].(*XTykAPIGateway).Server.Authentication.OIDC.Scopes.Claims = oidc.Scopes.Claims
571659
assert.Equal(t, oas, convertedOAS)
572660
}
573661

0 commit comments

Comments
 (0)