From 23c8796ef36e11e003814665fe13b8a946266a34 Mon Sep 17 00:00:00 2001 From: minhaj-shakeel Date: Wed, 3 Feb 2021 18:14:03 +0530 Subject: [PATCH] feat(GraphQL): Allow standard claims into auth variables (#7381) This PR adds support for adding `standard claims` of a `jwt` token in the `Auth Variables`. For eg, if the token contains claims given below and the namespace given in the authorization header is `https://xyz.io/jwt/claims`: ``` { "https://xyz.io/jwt/claims": [ .... ], "ROLE": "ADMIN", "USERROLE": "user1", "email": "random@example.com", "email_verified": true, "sub": "1234567890", "aud": "63do0q16n6ebjgkumu05kkeian", "iat": 1611694692, "exp": 2611730692 } ``` Then the auth variables will also include the rest of the given claims along with the claims provided under `https://xyz.io/jwt/claims`. --- graphql/authorization/auth.go | 22 ++++++++++++++--- graphql/e2e/auth/auth_test.go | 45 +++++++++++++++++++++++++++++++++++ graphql/resolve/auth_test.go | 42 +++++++++++++------------------- 3 files changed, 81 insertions(+), 28 deletions(-) diff --git a/graphql/authorization/auth.go b/graphql/authorization/auth.go index 7d7fe81a2df..238f9fe4674 100644 --- a/graphql/authorization/auth.go +++ b/graphql/authorization/auth.go @@ -278,8 +278,12 @@ type CustomClaims struct { jwt.StandardClaims } +// UnmarshalJSON unmarshalls the claims present in the JWT. +// It also adds standard claims to the `AuthVariables`. If +// there is an auth variable with name same as one of auth +// variable then the auth variable supersedes the standard claim. func (c *CustomClaims) UnmarshalJSON(data []byte) error { - // Unmarshal the standard claims first. + // Unmarshal the standard claims first if err := json.Unmarshal(data, &c.StandardClaims); err != nil { return err } @@ -291,14 +295,26 @@ func (c *CustomClaims) UnmarshalJSON(data []byte) error { // Unmarshal the auth variables for a particular namespace. if authValue, ok := result[authMeta.namespace()]; ok { - if authJson, ok := authValue.(string); ok { - if err := json.Unmarshal([]byte(authJson), &c.AuthVariables); err != nil { + if authJSON, ok := authValue.(string); ok { + if err := json.Unmarshal([]byte(authJSON), &c.AuthVariables); err != nil { return err } } else { c.AuthVariables, _ = authValue.(map[string]interface{}) } } + + // `result` contains all the cliams, delete the claim of the namespace mentioned + // in the Authorization Header. + delete(result, authMeta.namespace()) + // add AuthVariables into the `result` map, Now it contains all the AuthVariables + // and other claims present in the token. + for k, v := range c.AuthVariables { + result[k] = v + } + + // update `AuthVariables` with `result` map + c.AuthVariables = result return nil } diff --git a/graphql/e2e/auth/auth_test.go b/graphql/e2e/auth/auth_test.go index 7f1d9f7274a..66ca0439383 100644 --- a/graphql/e2e/auth/auth_test.go +++ b/graphql/e2e/auth/auth_test.go @@ -159,6 +159,7 @@ type TestCase struct { ans bool result string name string + jwt string filter map[string]interface{} variables map[string]interface{} query string @@ -494,6 +495,50 @@ func TestAuthOnInterfaces(t *testing.T) { }) } } + +func TestQueryWithStandardClaims(t *testing.T) { + if metaInfo.Algo == "RS256" { + t.Skip() + } + testCases := []TestCase{ + { + query: ` + query { + queryProject (order: {asc: name}) { + name + } + }`, + jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjozNTE2MjM5MDIyLCJlbWFpbCI6InRlc3RAZGdyYXBoLmlvIiwiVVNFUiI6InVzZXIxIiwiUk9MRSI6IkFETUlOIn0.cH_EcC8Sd0pawJs96XPhpRsYVXuTybT1oUkluBDS8B4", + result: `{"queryProject":[{"name":"Project1"},{"name":"Project2"}]}`, + }, + { + query: ` + query { + queryProject { + name + } + }`, + jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjozNTE2MjM5MDIyLCJlbWFpbCI6InRlc3RAZGdyYXBoLmlvIiwiVVNFUiI6InVzZXIxIn0.wabcAkINZ6ycbEuziTQTSpv8T875Ky7JQu68ynoyDQE", + result: `{"queryProject":[{"name":"Project1"}]}`, + }, + } + + for _, tcase := range testCases { + queryParams := &common.GraphQLParams{ + Headers: make(http.Header), + Query: tcase.query, + } + queryParams.Headers.Set(metaInfo.Header, tcase.jwt) + + gqlResponse := queryParams.ExecuteAsPost(t, common.GraphqlURL) + common.RequireNoGQLErrors(t, gqlResponse) + + if diff := cmp.Diff(tcase.result, string(gqlResponse.Data)); diff != "" { + t.Errorf("Test: %s result mismatch (-want +got):\n%s", tcase.name, diff) + } + } +} + func TestAuthRulesWithMissingJWT(t *testing.T) { testCases := []TestCase{ {name: "Query non auth field without JWT Token", diff --git a/graphql/resolve/auth_test.go b/graphql/resolve/auth_test.go index 67e4468e02e..fd6f4a89117 100644 --- a/graphql/resolve/auth_test.go +++ b/graphql/resolve/auth_test.go @@ -167,9 +167,15 @@ func TestStringCustomClaim(t *testing.T) { test.LoadSchemaFromString(t, string(authSchema)) testutil.SetAuthMeta(string(authSchema)) - // Token with string custom claim - // "https://xyz.io/jwt/claims": "{\"USER\": \"50950b40-262f-4b26-88a7-cbbb780b2176\", \"ROLE\": \"ADMIN\"}", - token := "eyJraWQiOiIyRWplN2tIRklLZS92MFRVT3JRYlVJWWJxSWNNUHZ2TFBjM3RSQ25EclBBPSIsImFsZyI6IkhTMjU2In0.eyJzdWIiOiI1MDk1MGI0MC0yNjJmLTRiMjYtODhhNy1jYmJiNzgwYjIxNzYiLCJjb2duaXRvOmdyb3VwcyI6WyJBRE1JTiJdLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6Ly9jb2duaXRvLWlkcC5hcC1zb3V0aGVhc3QtMi5hbWF6b25hd3MuY29tL2FwLXNvdXRoZWFzdC0yX0dmbWVIZEZ6NCIsImNvZ25pdG86dXNlcm5hbWUiOiI1MDk1MGI0MC0yNjJmLTRiMjYtODhhNy1jYmJiNzgwYjIxNzYiLCJodHRwczovL3h5ei5pby9qd3QvY2xhaW1zIjoie1wiVVNFUlwiOiBcIjUwOTUwYjQwLTI2MmYtNGIyNi04OGE3LWNiYmI3ODBiMjE3NlwiLCBcIlJPTEVcIjogXCJBRE1JTlwifSIsImF1ZCI6IjYzZG8wcTE2bjZlYmpna3VtdTA1a2tlaWFuIiwiZXZlbnRfaWQiOiIzMWM5ZDY4NC0xZDQ1LTQ2ZjctOGMyYi1jYzI3YjFmNmYwMWIiLCJ0b2tlbl91c2UiOiJpZCIsImF1dGhfdGltZSI6MTU5MDMzMzM1NiwibmFtZSI6IkRhdmlkIFBlZWsiLCJleHAiOjQ1OTAzNzYwMzIsImlhdCI6MTU5MDM3MjQzMiwiZW1haWwiOiJkYXZpZEB0eXBlam9pbi5jb20ifQ.g6rAkPdNIJ6wvXOo6F4XmoVqqbGs_CdUHx_k7NrvLY8" + // Token with custom claim: + // "https://xyz.io/jwt/claims": { + // "USERNAME": "Random User", + // "email": "random@dgraph.io" + // } + // + // It also contains standard claim : "email": "test@dgraph.io", but the + // value of "email" gets overwritten by the value present inside custom claim. + token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjM1MTYyMzkwMjIsImVtYWlsIjoidGVzdEBkZ3JhcGguaW8iLCJodHRwczovL3h5ei5pby9qd3QvY2xhaW1zIjp7IlVTRVJOQU1FIjoiUmFuZG9tIFVzZXIiLCJlbWFpbCI6InJhbmRvbUBkZ3JhcGguaW8ifX0.6XvP9wlvHx8ZBBMH9iyy49cRiIk7H6NNoZf69USkg2c" md := metadata.New(map[string]string{"authorizationJwt": token}) ctx := metadata.NewIncomingContext(context.Background(), md) @@ -177,9 +183,13 @@ func TestStringCustomClaim(t *testing.T) { require.NoError(t, err) authVar := customClaims.AuthVariables result := map[string]interface{}{ - "ROLE": "ADMIN", - "USER": "50950b40-262f-4b26-88a7-cbbb780b2176", + "sub": "1234567890", + "name": "John Doe", + "USERNAME": "Random User", + "email": "random@dgraph.io", } + delete(authVar, "exp") + delete(authVar, "iat") require.Equal(t, authVar, result) // reset auth meta, so that it won't effect other tests authorization.SetAuthMeta(&authorization.AuthMeta{}) @@ -232,18 +242,8 @@ func TestAudienceClaim(t *testing.T) { md := metadata.New(map[string]string{"authorizationJwt": tcase.token}) ctx := metadata.NewIncomingContext(context.Background(), md) - customClaims, err := authorization.ExtractCustomClaims(ctx) + _, err := authorization.ExtractCustomClaims(ctx) require.Equal(t, tcase.err, err) - if err != nil { - return - } - - authVar := customClaims.AuthVariables - result := map[string]interface{}{ - "ROLE": "ADMIN", - "USER": "50950b40-262f-4b26-88a7-cbbb780b2176", - } - require.Equal(t, authVar, result) }) } // reset auth meta, so that it won't effect other tests @@ -340,18 +340,10 @@ func TestJWTExpiry(t *testing.T) { md := metadata.New(map[string]string{"authorizationJwt": tcase.token}) ctx := metadata.NewIncomingContext(context.Background(), md) - customClaims, err := authorization.ExtractCustomClaims(ctx) + _, err := authorization.ExtractCustomClaims(ctx) if tcase.invalid { require.True(t, strings.Contains(err.Error(), "token is expired")) - return - } - - authVar := customClaims.AuthVariables - result := map[string]interface{}{ - "ROLE": "ADMIN", - "USER": "50950b40-262f-4b26-88a7-cbbb780b2176", } - require.Equal(t, authVar, result) }) }