Repository: Nerzal/gocloak
Pull request #196 (2020-07-28 10:58:12)

support audience array & strings
**Problem**
We currently only support to decode claims, where Audience is a single string value. 
But Audience can also be a []string. 

**Solution**

Upgrade jwt lib to v4 and add optional parameter audienceToCheck to DecodeAccesstoken and DecodeAccessTokenCustomClaims this should make these methods RFC7519 compliant.

More Information: 

```
4.1.3.  "aud" (Audience) Claim
   The "aud" (audience) claim identifies the recipients that the JWT is
   intended for.  Each principal intended to process the JWT MUST
   identify itself with a value in the audience claim.  If the principal
   processing the claim does not identify itself with a value in the
   "aud" claim when this claim is present, then the JWT MUST be
   rejected.  In the general case, the "aud" value is an array of case-
   sensitive strings, each containing a StringOrURI value.  In the
   special case when the JWT has one audience, the "aud" value MAY be a
   single case-sensitive string containing a StringOrURI value.  The
   interpretation of audience values is generally application specific.
   Use of this claim is OPTIONAL.
```


Patch:
diff --git a/client.go b/client.go
index 799e078..98f299d 100644
--- a/client.go
+++ b/client.go
@@ -9,7 +9,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/dgrijalva/jwt-go/v4"
 	"github.com/go-resty/resty/v2"
 	"github.com/pkg/errors"
 	"github.com/segmentio/ksuid"
@@ -277,7 +277,7 @@ func (client *gocloak) RetrospectToken(ctx context.Context, accessToken, clientI
 }
 
 // DecodeAccessToken decodes the accessToken
-func (client *gocloak) DecodeAccessToken(ctx context.Context, accessToken, realm string) (*jwt.Token, *jwt.MapClaims, error) {
+func (client *gocloak) DecodeAccessToken(ctx context.Context, accessToken, realm, expectedAudience string) (*jwt.Token, *jwt.MapClaims, error) {
 	const errMessage = "could not decode access token"
 
 	decodedHeader, err := jwx.DecodeAccessTokenHeader(accessToken)
@@ -297,11 +297,11 @@ func (client *gocloak) DecodeAccessToken(ctx context.Context, accessToken, realm
 		return nil, nil, errors.Wrap(errors.New("cannot find a key to decode the token"), errMessage)
 	}
 
-	return jwx.DecodeAccessToken(accessToken, usedKey.E, usedKey.N)
+	return jwx.DecodeAccessToken(accessToken, usedKey.E, usedKey.N, expectedAudience)
 }
 
 // DecodeAccessTokenCustomClaims decodes the accessToken and writes claims into the given claims
-func (client *gocloak) DecodeAccessTokenCustomClaims(ctx context.Context, accessToken, realm string, claims jwt.Claims) (*jwt.Token, error) {
+func (client *gocloak) DecodeAccessTokenCustomClaims(ctx context.Context, accessToken, realm, expectedAudience string, claims jwt.Claims) (*jwt.Token, error) {
 	const errMessage = "could not decode access token with custom claims"
 
 	decodedHeader, err := jwx.DecodeAccessTokenHeader(accessToken)
@@ -321,7 +321,7 @@ func (client *gocloak) DecodeAccessTokenCustomClaims(ctx context.Context, access
 		return nil, errors.Wrap(errors.New("cannot find a key to decode the token"), errMessage)
 	}
 
-	return jwx.DecodeAccessTokenCustomClaims(accessToken, usedKey.E, usedKey.N, claims)
+	return jwx.DecodeAccessTokenCustomClaims(accessToken, usedKey.E, usedKey.N, claims, expectedAudience)
 }
 
 func (client *gocloak) GetToken(ctx context.Context, realm string, options TokenOptions) (*JWT, error) {
@@ -402,14 +402,16 @@ func (client *gocloak) LoginClientSignedJWT(
 	realm string,
 	key interface{},
 	signedMethod jwt.SigningMethod,
-	expiresAt int64,
+	expiresAt *jwt.Time,
 ) (*JWT, error) {
 	claims := jwt.StandardClaims{
 		ExpiresAt: expiresAt,
 		Issuer:    clientID,
 		Subject:   clientID,
-		Id:        ksuid.New().String(),
-		Audience:  client.getRealmURL(realm),
+		ID:        ksuid.New().String(),
+		Audience: jwt.ClaimStrings{
+			client.getRealmURL(realm),
+		},
 	}
 	assertion, err := jwx.SignClaims(claims, key, signedMethod)
 	if err != nil {
diff --git a/go.mod b/go.mod
index 2347077..f085061 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,7 @@ module github.com/Nerzal/gocloak/v6
 go 1.14
 
 require (
-	github.com/dgrijalva/jwt-go v3.2.0+incompatible
+	github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1
 	github.com/go-resty/resty/v2 v2.3.0
 	github.com/pkg/errors v0.9.1
 	github.com/segmentio/ksuid v1.0.2
diff --git a/go.sum b/go.sum
index 8c8177f..d5fc23a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
-github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
+github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
 github.com/go-resty/resty/v2 v2.3.0 h1:JOOeAvjSlapTT92p8xiS19Zxev1neGikoHsXJeOq8So=
 github.com/go-resty/resty/v2 v2.3.0/go.mod h1:UpN9CgLZNsv4e9XG50UU8xdI0F43UQ4HmxLBDwaroHU=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -27,6 +27,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
diff --git a/gocloak.go b/gocloak.go
index b9d6d01..0da5cc9 100644
--- a/gocloak.go
+++ b/gocloak.go
@@ -3,7 +3,7 @@ package gocloak
 import (
 	"context"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/dgrijalva/jwt-go/v4"
 	"github.com/go-resty/resty/v2"
 )
 
@@ -34,15 +34,15 @@ type GoCloak interface {
 	// LoginClient sends a request to the token endpoint using client credentials
 	LoginClient(ctx context.Context, clientID, clientSecret, realm string) (*JWT, error)
 	// LoginClientSignedJWT performs a login with client credentials and signed jwt claims
-	LoginClientSignedJWT(ctx context.Context, clientID, realm string, key interface{}, signedMethod jwt.SigningMethod, expiresAt int64) (*JWT, error)
+	LoginClientSignedJWT(ctx context.Context, clientID, realm string, key interface{}, signedMethod jwt.SigningMethod, expiresAt *jwt.Time) (*JWT, error)
 	// LoginAdmin login as admin
 	LoginAdmin(ctx context.Context, username, password, realm string) (*JWT, error)
 	// RefreshToken used to refresh the token
 	RefreshToken(ctx context.Context, refreshToken, clientID, clientSecret, realm string) (*JWT, error)
 	// DecodeAccessToken decodes the accessToken
-	DecodeAccessToken(ctx context.Context, accessToken, realm string) (*jwt.Token, *jwt.MapClaims, error)
+	DecodeAccessToken(ctx context.Context, accessToken, realm, expectedAudience string) (*jwt.Token, *jwt.MapClaims, error)
 	// DecodeAccessTokenCustomClaims decodes the accessToken and fills the given claims
-	DecodeAccessTokenCustomClaims(ctx context.Context, accessToken, realm string, claims jwt.Claims) (*jwt.Token, error)
+	DecodeAccessTokenCustomClaims(ctx context.Context, accessToken, realm, expectedAudience string, claims jwt.Claims) (*jwt.Token, error)
 	// DecodeAccessTokenCustomClaims calls the token introspection endpoint
 	RetrospectToken(ctx context.Context, accessToken, clientID, clientSecret, realm string) (*RetrospecTokenResult, error)
 	// GetIssuer calls the issuer endpoint for the given realm
diff --git a/pkg/jwx/jwx.go b/pkg/jwx/jwx.go
index c6537c3..208898b 100644
--- a/pkg/jwx/jwx.go
+++ b/pkg/jwx/jwx.go
@@ -10,7 +10,7 @@ import (
 	"math/big"
 	"strings"
 
-	jwt "github.com/dgrijalva/jwt-go"
+	jwt "github.com/dgrijalva/jwt-go/v4"
 	"github.com/pkg/errors"
 )
 
@@ -75,7 +75,7 @@ func decodePublicKey(e, n *string) (*rsa.PublicKey, error) {
 }
 
 // DecodeAccessToken currently only supports RSA - sorry for that
-func DecodeAccessToken(accessToken string, e, n *string) (*jwt.Token, *jwt.MapClaims, error) {
+func DecodeAccessToken(accessToken string, e, n *string, expectedAudience string) (*jwt.Token, *jwt.MapClaims, error) {
 	const errMessage = "could not decode accessToken"
 
 	rsaPublicKey, err := decodePublicKey(e, n)
@@ -84,13 +84,19 @@ func DecodeAccessToken(accessToken string, e, n *string) (*jwt.Token, *jwt.MapCl
 	}
 
 	claims := &jwt.MapClaims{}
+
+	audValidation := jwt.WithoutAudienceValidation()
+	if expectedAudience != "" {
+		audValidation = jwt.WithAudience(expectedAudience)
+	}
+
 	token2, err := jwt.ParseWithClaims(accessToken, claims, func(token *jwt.Token) (interface{}, error) {
 		// Don't forget to validate the alg is what you expect:
 		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
 			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 		}
 		return rsaPublicKey, nil
-	})
+	}, audValidation)
 
 	if err != nil {
 		return nil, nil, errors.Wrap(err, errMessage)
@@ -100,7 +106,7 @@ func DecodeAccessToken(accessToken string, e, n *string) (*jwt.Token, *jwt.MapCl
 }
 
 // DecodeAccessTokenCustomClaims currently only supports RSA - sorry for that
-func DecodeAccessTokenCustomClaims(accessToken string, e, n *string, customClaims jwt.Claims) (*jwt.Token, error) {
+func DecodeAccessTokenCustomClaims(accessToken string, e, n *string, customClaims jwt.Claims, expectedAudience string) (*jwt.Token, error) {
 	const errMessage = "could not decode accessToken with custom claims"
 
 	rsaPublicKey, err := decodePublicKey(e, n)
@@ -108,13 +114,18 @@ func DecodeAccessTokenCustomClaims(accessToken string, e, n *string, customClaim
 		return nil, errors.Wrap(err, errMessage)
 	}
 
+	audValidation := jwt.WithoutAudienceValidation()
+	if expectedAudience != "" {
+		audValidation = jwt.WithAudience(expectedAudience)
+	}
+
 	token2, err := jwt.ParseWithClaims(accessToken, customClaims, func(token *jwt.Token) (interface{}, error) {
 		// Don't forget to validate the alg is what you expect:
 		if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
 			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
 		}
 		return rsaPublicKey, nil
-	})
+	}, audValidation)
 
 	if err != nil {
 		return nil, errors.Wrap(err, errMessage)
diff --git a/pkg/jwx/models.go b/pkg/jwx/models.go
index 1f0e5b9..57d5105 100644
--- a/pkg/jwx/models.go
+++ b/pkg/jwx/models.go
@@ -1,6 +1,6 @@
 package jwx
 
-import jwt "github.com/dgrijalva/jwt-go"
+import jwt "github.com/dgrijalva/jwt-go/v4"
 
 // DecodedAccessTokenHeader is the decoded header from the access token
 type DecodedAccessTokenHeader struct {


Test patch:
diff --git a/client_test.go b/client_test.go
index 1f253f2..077eebf 100644
--- a/client_test.go
+++ b/client_test.go
@@ -20,7 +20,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/dgrijalva/jwt-go"
+	"github.com/dgrijalva/jwt-go/v4"
 	"github.com/go-resty/resty/v2"
 	"github.com/stretchr/testify/require"
 	"golang.org/x/crypto/pkcs12"
@@ -692,6 +692,7 @@ func TestGocloak_DecodeAccessToken(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
+		"",
 	)
 	require.NoError(t, err)
 	t.Log(resultToken)
@@ -708,6 +709,7 @@ func TestGocloak_DecodeAccessTokenCustomClaims(t *testing.T) {
 		context.Background(),
 		token.AccessToken,
 		cfg.GoCloak.Realm,
+		"",
 		claims,
 	)
 	require.NoError(t, err)
@@ -811,7 +813,9 @@ func TestGocloak_LoginSignedJWT(t *testing.T) {
 		cfg.GoCloak.Realm,
 		rsaKey,
 		jwt.SigningMethodRS256,
-		time.Now().Add(time.Hour).Unix(),
+		&jwt.Time{
+			Time: time.Now().Add(time.Hour),
+		},
 	)
 	require.NoError(t, err, "Login failed")
 }