Skip to content

Commit 18312b4

Browse files
asoormvaskejeffy-mathew
authored andcommitted
[wip] support more complex scope / role objects from JWT claims (#3529)
* scopes can be nested and either space separated strings or array slices * functions rename to have better reading and understanding Co-authored-by: Milan Vasic <[email protected]> Co-authored-by: Jeffy Mathew <[email protected]> (cherry picked from commit 8ba3c49)
1 parent 51d30c0 commit 18312b4

File tree

2 files changed

+112
-7
lines changed

2 files changed

+112
-7
lines changed

gateway/mw_jwt.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,16 +315,47 @@ func (k *JWTMiddleware) getUserIdFromClaim(claims jwt.MapClaims) (string, error)
315315
return "", errors.New(message)
316316
}
317317

318-
func getScopeFromClaim(claims jwt.MapClaims, scopeClaimName string) []string {
319-
// get claim with scopes and turn it into slice of strings
320-
if scope, found := claims[scopeClaimName].(string); found {
321-
return strings.Split(scope, " ") // by standard is space separated list of values
318+
func toStrings(v interface{}) []string {
319+
switch e := v.(type) {
320+
case string:
321+
return strings.Split(e, " ")
322+
case []interface{}:
323+
var r []string
324+
for _, x := range e {
325+
r = append(r, toStrings(x)...)
326+
}
327+
return r
322328
}
323-
324-
// claim with scopes is optional so return nothing if it is not present
325329
return nil
326330
}
327331

332+
func nestedMapLookup(m map[string]interface{}, ks ...string) interface{} {
333+
var c interface{} = m
334+
for _, k := range ks {
335+
if _, ok := c.(map[string]interface{}); !ok {
336+
//fmt.Errorf("key not found; remaining keys: %v", ks)
337+
return nil
338+
}
339+
c = getMapContext(c, k)
340+
}
341+
return c
342+
}
343+
344+
func getMapContext(m interface{}, k string) (rval interface{}) {
345+
switch e := m.(type) {
346+
case map[string]interface{}:
347+
return e[k]
348+
default:
349+
return e
350+
}
351+
}
352+
353+
func getScopeFromClaim(claims jwt.MapClaims, scopeClaimName string) []string {
354+
lookedUp := nestedMapLookup(claims, strings.Split(scopeClaimName, ".")...)
355+
356+
return toStrings(lookedUp)
357+
}
358+
328359
func mapScopeToPolicies(mapping map[string]string, scope []string) []string {
329360
polIDs := []string{}
330361

gateway/mw_jwt_test.go

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"testing"
1212
"time"
1313

14-
jwt "github.com/dgrijalva/jwt-go"
14+
"github.com/dgrijalva/jwt-go"
1515
"github.com/lonelycode/go-uuid/uuid"
1616
"github.com/stretchr/testify/assert"
1717

@@ -1223,7 +1223,81 @@ func TestJWTScopeToPolicyMapping(t *testing.T) {
12231223
},
12241224
)
12251225
})
1226+
}
1227+
1228+
func TestGetScopeFromClaim(t *testing.T) {
1229+
type tableTest struct {
1230+
jwt string
1231+
key string
1232+
expectedClaims []string
1233+
name string
1234+
}
1235+
1236+
tests := []tableTest{
1237+
{
1238+
jwt: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwic2NvcGUiOiJmb28gYmFyIGJheiJ9.iS5FYY99ccB1oTGtMmNjM1lppS18FSKPytrV9oQouSM`,
1239+
key: "scope",
1240+
expectedClaims: []string{"foo", "bar", "baz"},
1241+
name: "space separated",
1242+
},
1243+
{
1244+
jwt: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwic2NvcGUiOlsiZm9vIiwiYmFyIiwiYmF6Il19.Lo_7J1FpUcsKWC4E9nMiouyVdUClA3KujHu9EwqHEwo`,
1245+
key: "scope",
1246+
expectedClaims: []string{"foo", "bar", "baz"},
1247+
name: "slice strings",
1248+
},
1249+
{
1250+
jwt: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwic2NvcGUxIjp7InNjb3BlMiI6ImZvbyBiYXIgYmF6In19.IsCBEl-GozS-sgZaTHoLwuBKmxYLOCYYVCiLLVmGu8o`,
1251+
key: "scope1.scope2",
1252+
expectedClaims: []string{"foo", "bar", "baz"},
1253+
name: "nested space separated",
1254+
},
1255+
{
1256+
jwt: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMiwic2NvcGUxIjp7InNjb3BlMiI6WyJmb28iLCJiYXIiLCJiYXoiXX19.VDBnH2U7KWl-fajAHGq6PzzWp4mnNCkfKAodfhHc0gY`,
1257+
key: "scope1.scope2",
1258+
expectedClaims: []string{"foo", "bar", "baz"},
1259+
name: "nested slice strings",
1260+
},
1261+
}
1262+
1263+
pubKey := []byte(`mysecret`)
1264+
1265+
for i, mytest := range tests {
1266+
t.Run(fmt.Sprintf("%d %s", i, mytest.name), func(t *testing.T) {
1267+
tok, err := jwt.Parse(mytest.jwt, func(token *jwt.Token) (interface{}, error) {
1268+
return pubKey, nil
1269+
})
1270+
if err != nil {
1271+
t.Fatal(err.Error())
1272+
}
1273+
1274+
scopes := getScopeFromClaim(tok.Claims.(jwt.MapClaims), mytest.key)
1275+
if !testEq(mytest.expectedClaims, scopes) {
1276+
t.Logf("expected: %v", mytest.expectedClaims)
1277+
t.Logf("actual: %v", scopes)
1278+
t.Fatal(i, "slices not equal")
1279+
}
1280+
})
1281+
}
1282+
}
1283+
1284+
func testEq(a, b []string) bool {
1285+
// If one is nil, the other must also be nil.
1286+
if (a == nil) != (b == nil) {
1287+
return false
1288+
}
1289+
1290+
if len(a) != len(b) {
1291+
return false
1292+
}
1293+
1294+
for i := range a {
1295+
if a[i] != b[i] {
1296+
return false
1297+
}
1298+
}
12261299

1300+
return true
12271301
}
12281302

12291303
func TestJWTExistingSessionRSAWithRawSourcePolicyIDChanged(t *testing.T) {

0 commit comments

Comments
 (0)