Skip to content

Commit

Permalink
feat(GraphQL): add support for all RSA and HMAC algorithms supported …
Browse files Browse the repository at this point in the history
…by github.com/dgrijalva/jwt-go/v4 (#6750)

The GraphQL JWT authorisation feature only supported HS256 and RS256 when the underlying library dealing with JWTs (github.com/dgrijalva/jwt-go/v4) supports many more.

This PR adds support for all HMAC and RSA signing keys. I have purposefully left out the other algorithms since I don't know much about them and whether they need treating differently when it comes to keys etc.

Signed-off-by: dan-j <[email protected]>
  • Loading branch information
dan-j authored Oct 28, 2020
1 parent d688c46 commit f74f202
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 37 deletions.
97 changes: 69 additions & 28 deletions graphql/authorization/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,19 @@ type authVariablekey string
const (
AuthJwtCtxKey = ctxKey("authorizationJwt")
AuthVariables = authVariablekey("authVariable")
RSA256 = "RS256"
HMAC256 = "HS256"
AuthMetaHeader = "# Dgraph.Authorization "
)

var (
authMeta = &AuthMeta{}
authMeta = &AuthMeta{}
supportedAlgorithms = map[string]jwt.SigningMethod{
jwt.SigningMethodRS256.Name: jwt.SigningMethodRS256,
jwt.SigningMethodRS384.Name: jwt.SigningMethodRS384,
jwt.SigningMethodRS512.Name: jwt.SigningMethodRS512,
jwt.SigningMethodHS256.Name: jwt.SigningMethodHS256,
jwt.SigningMethodHS384.Name: jwt.SigningMethodHS384,
jwt.SigningMethodHS512.Name: jwt.SigningMethodHS512,
}
)

type AuthMeta struct {
Expand All @@ -61,6 +67,7 @@ type AuthMeta struct {
Header string
Namespace string
Algo string
SigningMethod jwt.SigningMethod `json:"-"` // Ignoring this field
Audience []string
sync.RWMutex
}
Expand Down Expand Up @@ -109,7 +116,15 @@ func Parse(schema string) (*AuthMeta, error) {

err := json.Unmarshal([]byte(authInfo[len(AuthMetaHeader):]), &meta)
if err == nil {
return &meta, meta.validate()
if err := meta.validate(); err != nil {
return nil, err
}

if algoErr := meta.initSigningMethod(); algoErr != nil {
return nil, algoErr
}

return &meta, nil
}

fmt.Println("Falling back to parsing `Dgraph.Authorization` in old format." +
Expand Down Expand Up @@ -141,13 +156,11 @@ func Parse(schema string) (*AuthMeta, error) {
meta.Namespace = authInfo[idx[0][6]:idx[0][7]]
meta.Algo = authInfo[idx[0][8]:idx[0][9]]
meta.VerificationKey = authInfo[idx[0][10]:idx[0][11]]
if meta.Algo == HMAC256 {
return &meta, nil
}
if meta.Algo != RSA256 {
return nil, errors.Errorf(
"invalid jwt algorithm: found %s, but supported options are HS256 or RS256", meta.Algo)

if err := meta.initSigningMethod(); err != nil {
return nil, err
}

return &meta, nil
}

Expand All @@ -157,18 +170,17 @@ func ParseAuthMeta(schema string) (*AuthMeta, error) {
return nil, err
}

if metaInfo.Algo != RSA256 {
return metaInfo, nil
}

// The jwt library internally uses `bytes.IndexByte(data, '\n')` to fetch new line and fails
// if we have newline "\n" as ASCII value {92,110} instead of the actual ASCII value of 10.
// To fix this we replace "\n" with new line's ASCII value.
bytekey := bytes.ReplaceAll([]byte(metaInfo.VerificationKey), []byte{92, 110}, []byte{10})
if _, ok := metaInfo.SigningMethod.(*jwt.SigningMethodRSA); ok {
// The jwt library internally uses `bytes.IndexByte(data, '\n')` to fetch new line and fails
// if we have newline "\n" as ASCII value {92,110} instead of the actual ASCII value of 10.
// To fix this we replace "\n" with new line's ASCII value.
bytekey := bytes.ReplaceAll([]byte(metaInfo.VerificationKey), []byte{92, 110}, []byte{10})

if metaInfo.RSAPublicKey, err = jwt.ParseRSAPublicKeyFromPEM(bytekey); err != nil {
return nil, err
if metaInfo.RSAPublicKey, err = jwt.ParseRSAPublicKeyFromPEM(bytekey); err != nil {
return nil, err
}
}

return metaInfo, nil
}

Expand Down Expand Up @@ -232,6 +244,7 @@ func SetAuthMeta(m *AuthMeta) {
authMeta.Header = m.Header
authMeta.Namespace = m.Namespace
authMeta.Algo = m.Algo
authMeta.SigningMethod = m.SigningMethod
authMeta.Audience = m.Audience
}

Expand Down Expand Up @@ -380,15 +393,14 @@ func validateJWTCustomClaims(jwtStr string) (*CustomClaims, error) {
return nil, errors.Errorf("unexpected signing method: Expected %s Found %s",
amAlgo, algo)
}
if algo == HMAC256 {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); ok {
return []byte(authMeta.verificationKey()), nil
}
} else if algo == RSA256 {
if _, ok := token.Method.(*jwt.SigningMethodRSA); ok {
return authMeta.rsaPublicKey(), nil
}

switch authMeta.SigningMethod.(type) {
case *jwt.SigningMethodHMAC:
return []byte(authMeta.verificationKey()), nil
case *jwt.SigningMethodRSA:
return authMeta.rsaPublicKey(), nil
}

return nil, errors.Errorf("couldn't parse signing method from token header: %s", algo)
}, jwt.WithoutAudienceValidation())
}
Expand Down Expand Up @@ -493,3 +505,32 @@ func (a *AuthMeta) isExpired() bool {
}
return time.Now().After(a.expiryTime)
}

// initSigningMethod takes the current Algo value, validates it's a supported SigningMethod, then sets the SigningMethod
// field.
func (a *AuthMeta) initSigningMethod() error {
a.Lock()
defer a.Unlock()

// configurations using JWK URLs do not use signing methods.
if a.JWKUrl != "" {
return nil
}

signingMethod, ok := supportedAlgorithms[a.Algo]
if !ok {
arr := make([]string, 0, len(supportedAlgorithms))
for k := range supportedAlgorithms {
arr = append(arr, k)
}

return errors.Errorf(
"invalid jwt algorithm: found %s, but supported options are: %s",
a.Algo, strings.Join(arr, ","),
)
}

a.SigningMethod = signingMethod

return nil
}
4 changes: 2 additions & 2 deletions graphql/e2e/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go/v4"
"io/ioutil"
"os"
"strings"
"testing"

"github.com/dgraph-io/dgraph/graphql/authorization"
"github.com/dgraph-io/dgraph/graphql/e2e/common"
"github.com/dgraph-io/dgraph/testutil"
"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -1314,7 +1314,7 @@ func TestMain(m *testing.M) {
panic(errors.Wrapf(err, "Unable to read file %s.", jsonFile))
}

jwtAlgo := []string{authorization.HMAC256, authorization.RSA256}
jwtAlgo := []string{jwt.SigningMethodHS256.Name, jwt.SigningMethodRS256.Name}
for _, algo := range jwtAlgo {
authSchema, err := testutil.AppendAuthInfo(schema, algo, "./sample_public_key.pem")
if err != nil {
Expand Down
3 changes: 2 additions & 1 deletion graphql/e2e/auth/debug_off/debugoff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package debugoff

import (
"encoding/json"
"github.com/dgrijalva/jwt-go/v4"
"io/ioutil"
"os"
"testing"
Expand Down Expand Up @@ -135,7 +136,7 @@ func TestMain(m *testing.M) {
panic(errors.Wrapf(err, "Unable to read file %s.", jsonFile))
}

jwtAlgo := []string{authorization.HMAC256, authorization.RSA256}
jwtAlgo := []string{jwt.SigningMethodHS256.Name, jwt.SigningMethodRS256.Name}
for _, algo := range jwtAlgo {
authSchema, err := testutil.AppendAuthInfo(schema, algo, "../sample_public_key.pem")
if err != nil {
Expand Down
13 changes: 7 additions & 6 deletions graphql/resolve/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/dgrijalva/jwt-go/v4"
"io/ioutil"
"strconv"
"strings"
Expand Down Expand Up @@ -158,7 +159,7 @@ func TestStringCustomClaim(t *testing.T) {
sch, err := ioutil.ReadFile("../e2e/auth/schema.graphql")
require.NoError(t, err, "Unable to read schema file")

authSchema, err := testutil.AppendAuthInfo(sch, authorization.HMAC256, "")
authSchema, err := testutil.AppendAuthInfo(sch, jwt.SigningMethodHS256.Name, "")
require.NoError(t, err)

test.LoadSchemaFromString(t, string(authSchema))
Expand All @@ -184,15 +185,15 @@ func TestAudienceClaim(t *testing.T) {
sch, err := ioutil.ReadFile("../e2e/auth/schema.graphql")
require.NoError(t, err, "Unable to read schema file")

authSchema, err := testutil.AppendAuthInfo(sch, authorization.HMAC256, "")
authSchema, err := testutil.AppendAuthInfo(sch, jwt.SigningMethodHS256.Name, "")
require.NoError(t, err)

test.LoadSchemaFromString(t, string(authSchema))
testutil.SetAuthMeta(string(authSchema))

// Verify that authorization information is set correctly.
metainfo := authorization.GetAuthMeta()
require.Equal(t, metainfo.Algo, authorization.HMAC256)
require.Equal(t, metainfo.Algo, jwt.SigningMethodHS256.Name)
require.Equal(t, metainfo.Header, "X-Test-Auth")
require.Equal(t, metainfo.Namespace, "https://xyz.io/jwt/claims")
require.Equal(t, metainfo.VerificationKey, "secretkey")
Expand Down Expand Up @@ -290,15 +291,15 @@ func TestJWTExpiry(t *testing.T) {
sch, err := ioutil.ReadFile("../e2e/auth/schema.graphql")
require.NoError(t, err, "Unable to read schema file")

authSchema, err := testutil.AppendAuthInfo(sch, authorization.HMAC256, "")
authSchema, err := testutil.AppendAuthInfo(sch, jwt.SigningMethodHS256.Name, "")
require.NoError(t, err)

test.LoadSchemaFromString(t, string(authSchema))
testutil.SetAuthMeta(string(authSchema))

// Verify that authorization information is set correctly.
metainfo := authorization.GetAuthMeta()
require.Equal(t, metainfo.Algo, authorization.HMAC256)
require.Equal(t, metainfo.Algo, jwt.SigningMethodHS256.Name)
require.Equal(t, metainfo.Header, "X-Test-Auth")
require.Equal(t, metainfo.Namespace, "https://xyz.io/jwt/claims")
require.Equal(t, metainfo.VerificationKey, "secretkey")
Expand Down Expand Up @@ -745,7 +746,7 @@ func TestAuthQueryRewriting(t *testing.T) {
sch, err := ioutil.ReadFile("../e2e/auth/schema.graphql")
require.NoError(t, err, "Unable to read schema file")

jwtAlgo := []string{authorization.HMAC256, authorization.RSA256}
jwtAlgo := []string{jwt.SigningMethodHS256.Name, jwt.SigningMethodRS256.Name}

for _, algo := range jwtAlgo {
result, err := testutil.AppendAuthInfo(sch, algo, "../e2e/auth/sample_public_key.pem")
Expand Down

0 comments on commit f74f202

Please sign in to comment.