Skip to content

Commit

Permalink
Merge 7febb97 into f9e008e
Browse files Browse the repository at this point in the history
  • Loading branch information
dencoded committed Dec 6, 2018
2 parents f9e008e + 7febb97 commit 7a66fe2
Show file tree
Hide file tree
Showing 7 changed files with 315 additions and 24 deletions.
26 changes: 15 additions & 11 deletions apidef/api_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,8 +305,10 @@ type ServiceDiscoveryConfiguration struct {
}

type OIDProviderConfig struct {
Issuer string `bson:"issuer" json:"issuer"`
ClientIDs map[string]string `bson:"client_ids" json:"client_ids"`
Issuer string `bson:"issuer" json:"issuer"`
ClientIDs map[string]string `bson:"client_ids" json:"client_ids"`
ScopeFieldName string `bson:"scope_field_name" json:"scope_field_name"`
ScopeToPolicyMapping map[string]string `bson:"scope_to_policy_mapping" json:"scope_to_policy_mapping"`
}

type OpenIDOptions struct {
Expand Down Expand Up @@ -352,6 +354,7 @@ type APIDefinition struct {
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"`
JWTScopeToPolicyMapping map[string]string `bson:"jwt_scope_to_policy_mapping" json:"jwt_scope_to_policy_mapping"`
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"`
Expand Down Expand Up @@ -665,15 +668,16 @@ func DummyAPI() APIDefinition {
}

return APIDefinition{
VersionData: versionData,
ConfigData: map[string]interface{}{},
AllowedIPs: []string{},
PinnedPublicKeys: map[string]string{},
ResponseProcessors: []ResponseProcessor{},
ClientCertificates: []string{},
BlacklistedIPs: []string{},
TagHeaders: []string{},
UpstreamCertificates: map[string]string{},
VersionData: versionData,
ConfigData: map[string]interface{}{},
AllowedIPs: []string{},
PinnedPublicKeys: map[string]string{},
ResponseProcessors: []ResponseProcessor{},
ClientCertificates: []string{},
BlacklistedIPs: []string{},
TagHeaders: []string{},
UpstreamCertificates: map[string]string{},
JWTScopeToPolicyMapping: map[string]string{},
CustomMiddleware: MiddlewareSection{
Post: []MiddlewareDefinition{},
Pre: []MiddlewareDefinition{},
Expand Down
3 changes: 3 additions & 0 deletions apidef/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ const Schema = `{
"jwt_not_before_validation_skew": {
"type": "number"
},
"jwt_scope_to_policy_mapping": {
"type": "object"
},
"use_keyless": {
"type": "boolean"
},
Expand Down
3 changes: 3 additions & 0 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ func (t BaseMiddleware) CheckSessionAndIdentityForValidKey(key string, r *http.R
session := cachedVal.(user.SessionState)
if err := t.ApplyPolicies(key, &session); err != nil {
t.Logger().Error(err)
return session, false
}
return session, true
}
Expand All @@ -444,6 +445,7 @@ func (t BaseMiddleware) CheckSessionAndIdentityForValidKey(key string, r *http.R
// Check for a policy, if there is a policy, pull it and overwrite the session values
if err := t.ApplyPolicies(key, &session); err != nil {
t.Logger().Error(err)
return session, false
}
t.Logger().Debug("Got key")
return session, true
Expand All @@ -465,6 +467,7 @@ func (t BaseMiddleware) CheckSessionAndIdentityForValidKey(key string, r *http.R
// Check for a policy, if there is a policy, pull it and overwrite the session values
if err := t.ApplyPolicies(key, &session); err != nil {
t.Logger().Error(err)
return session, false
}

t.Logger().Debug("Lifetime is: ", session.Lifetime(t.Spec.SessionLifetime))
Expand Down
57 changes: 55 additions & 2 deletions mw_jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
"time"

"github.com/dgrijalva/jwt-go"
cache "github.com/pmylund/go-cache"
"github.com/pmylund/go-cache"

"github.com/TykTechnologies/tyk/apidef"
"github.com/TykTechnologies/tyk/user"
Expand Down Expand Up @@ -244,6 +244,33 @@ func (k *JWTMiddleware) getUserIdFromClaim(claims jwt.MapClaims) (string, error)
return "", errors.New(message)
}

func getScopeFromClaim(claims jwt.MapClaims, scopeClaimName string) []string {
// get claim with scopes and turn it into slice of strings
if scope, found := claims[scopeClaimName].(string); found {
return strings.Split(scope, " ") // by standard is space separated list of values
}

// claim with scopes is optional so return nothing if it is not present
return nil
}

func mapScopeToPolicies(mapping map[string]string, scope []string) []string {
polIDs := []string{}

// add all policies matched from scope-policy mapping
policiesToApply := map[string]bool{}
for _, scopeItem := range scope {
if policyID, ok := mapping[scopeItem]; ok {
policiesToApply[policyID] = true
}
}
for id := range policiesToApply {
polIDs = append(polIDs, id)
}

return polIDs
}

// processCentralisedJWT Will check a JWT token centrally against the secret stored in the API Definition.
func (k *JWTMiddleware) processCentralisedJWT(r *http.Request, token *jwt.Token) (error, int) {
k.Logger().Debug("JWT authority is centralised")
Expand Down Expand Up @@ -278,6 +305,29 @@ func (k *JWTMiddleware) processCentralisedJWT(r *http.Request, token *jwt.Token)
newSession, err := generateSessionFromPolicy(basePolicyID,
k.Spec.OrgID,
true)

// apply policies from scope if scope-to-policy mapping is specified for this API
if k.Spec.JWTScopeToPolicyMapping != nil {
if scope := getScopeFromClaim(claims, "scope"); scope != nil {
polIDs := []string{
basePolicyID, // add base policy as a first one
}

// add all policies matched from scope-policy mapping
mappedPolIDs := mapScopeToPolicies(k.Spec.JWTScopeToPolicyMapping, scope)

polIDs = append(polIDs, mappedPolIDs...)
newSession.SetPolicies(polIDs...)

// multiple policies assigned to a key, check if it is applicable
if err := k.ApplyPolicies(sessionID, &newSession); err != nil {
k.reportLoginFailure(baseFieldData, r)
k.Logger().WithError(err).Error("Could not several policies from scope-claim mapping to JWT to session")
return errors.New("Key not authorized: could not apply several policies"), http.StatusForbidden
}
}
}

if err != nil {
k.reportLoginFailure(baseFieldData, r)
k.Logger().Error("Could not find a valid policy to apply to this token!")
Expand Down Expand Up @@ -575,7 +625,10 @@ func generateSessionFromPolicy(policyID, orgID string, enforceOrg bool) (user.Se
session.Per = policy.Per
session.QuotaMax = policy.QuotaMax
session.QuotaRenewalRate = policy.QuotaRenewalRate
session.AccessRights = policy.AccessRights
session.AccessRights = make(map[string]user.AccessDefinition)
for apiID, access := range policy.AccessRights {
session.AccessRights[apiID] = access
}
session.HMACEnabled = policy.HMACEnabled
session.IsInactive = policy.IsInactive
session.Tags = policy.Tags
Expand Down
192 changes: 192 additions & 0 deletions mw_jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package main
import (
"crypto/md5"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"reflect"
"testing"
"time"

Expand Down Expand Up @@ -905,6 +907,196 @@ func TestJWTExistingSessionRSAWithRawSourceInvalidPolicyID(t *testing.T) {
})
}

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

basePolicyID := createPolicy(func(p *user.Policy) {
p.AccessRights = map[string]user.AccessDefinition{
"base-api": {
Limit: &user.APILimit{
Rate: 111,
Per: 3600,
QuotaMax: -1,
},
},
}
p.Partitions = user.PolicyPartitions{
PerAPI: true,
}
})

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

p1ID := createPolicy(func(p *user.Policy) {
p.AccessRights = map[string]user.AccessDefinition{
spec1.APIID: {
Limit: &user.APILimit{
Rate: 100,
Per: 60,
QuotaMax: -1,
},
},
}
p.Partitions = user.PolicyPartitions{
PerAPI: true,
}
})

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

p2ID := createPolicy(func(p *user.Policy) {
p.AccessRights = map[string]user.AccessDefinition{
spec2.APIID: {
Limit: &user.APILimit{
Rate: 500,
Per: 30,
QuotaMax: -1,
},
},
}
p.Partitions = user.PolicyPartitions{
PerAPI: true,
}
})

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

spec := buildAPI(func(spec *APISpec) {
spec.APIID = "base-api"
spec.UseKeylessAccess = false
spec.EnableJWT = true
spec.JWTSigningMethod = RSASign
spec.JWTSource = base64.StdEncoding.EncodeToString([]byte(jwtRSAPubKey))
spec.JWTIdentityBaseField = "user_id"
spec.JWTPolicyFieldName = "policy_id"
spec.Proxy.ListenPath = "/base"
spec.JWTScopeToPolicyMapping = map[string]string{
"user:read": p1ID,
"user:write": p2ID,
}
})[0]

loadAPI(spec, spec1, spec2, spec3)

userID := "user-" + uuid.New()

jwtToken := createJWKToken(func(t *jwt.Token) {
t.Header["kid"] = "12345"
t.Claims.(jwt.MapClaims)["foo"] = "bar"
t.Claims.(jwt.MapClaims)["user_id"] = userID
t.Claims.(jwt.MapClaims)["policy_id"] = basePolicyID
t.Claims.(jwt.MapClaims)["exp"] = time.Now().Add(time.Hour * 72).Unix()
t.Claims.(jwt.MapClaims)["scope"] = "user:read user:write"
})

authHeaders := map[string]string{"authorization": jwtToken}
t.Run("Request with scope in JWT to create a key session", func(t *testing.T) {
ts.Run(t,
test.TestCase{
Headers: authHeaders,
Path: "/base",
Code: http.StatusOK,
})
})

// check that key has right set of policies assigned - there should be all three - base one and two from scope
sessionID := generateToken("", fmt.Sprintf("%x", md5.Sum([]byte(userID))))
t.Run("Request to check that session has got correct apply_policies value", func(t *testing.T) {
ts.Run(
t,
test.TestCase{
Method: http.MethodGet,
Path: "/tyk/keys/" + sessionID,
AdminAuth: true,
Code: http.StatusOK,
BodyMatchFunc: func(body []byte) bool {
expectedResp := map[interface{}]bool{
basePolicyID: true,
p1ID: true,
p2ID: true,
}

resp := map[string]interface{}{}
json.Unmarshal(body, &resp)
realResp := map[interface{}]bool{}
for _, val := range resp["apply_policies"].([]interface{}) {
realResp[val] = true
}

return reflect.DeepEqual(realResp, expectedResp)
},
},
)
})

// try to access api1 using JWT issued via base-api
t.Run("Request to api1", func(t *testing.T) {
ts.Run(
t,
test.TestCase{
Headers: authHeaders,
Method: http.MethodGet,
Path: "/api1",
Code: http.StatusOK,
},
)
})

// try to access api2 using JWT issued via base-api
t.Run("Request to api2", func(t *testing.T) {
ts.Run(
t,
test.TestCase{
Headers: authHeaders,
Method: http.MethodGet,
Path: "/api2",
Code: http.StatusOK,
},
)
})

// try to access api3 (which is not granted via base policy nor scope-policy mapping) using JWT issued via base-api
t.Run("Request to api3", func(t *testing.T) {
ts.Run(
t,
test.TestCase{
Headers: authHeaders,
Method: http.MethodGet,
Path: "/api3",
Code: http.StatusForbidden,
},
)
})
}

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

0 comments on commit 7a66fe2

Please sign in to comment.