From ab9769c0b7f84fb098ad816d017ba8342839283c Mon Sep 17 00:00:00 2001 From: Ross Tannenbaum Date: Tue, 18 Jun 2024 14:15:01 -0700 Subject: [PATCH] chore: converge on go-jose (#1297) --- auth/claimrule/claim_rule_test.go | 6 +- auth/tokenizer.go | 101 ++++++++++++++++-------------- go.mod | 3 +- go.sum | 5 +- 4 files changed, 59 insertions(+), 56 deletions(-) diff --git a/auth/claimrule/claim_rule_test.go b/auth/claimrule/claim_rule_test.go index c045cf74e..4b5be3bc1 100644 --- a/auth/claimrule/claim_rule_test.go +++ b/auth/claimrule/claim_rule_test.go @@ -4,8 +4,7 @@ import ( "encoding/json" "testing" - "gopkg.in/square/go-jose.v2" - + "github.com/go-jose/go-jose/v4" "github.com/stretchr/testify/assert" ) @@ -16,7 +15,8 @@ type dataSet struct { } func getRawToken(t *testing.T, jwt map[string]interface{}) string { - signer, err := jose.NewSigner(jose.SigningKey{Algorithm: "HS256", Key: []byte("secret")}, (&jose.SignerOptions{}).WithType("JWT")) + key := []byte(`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`) + signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT")) if err != nil { t.Fatal(err) } diff --git a/auth/tokenizer.go b/auth/tokenizer.go index 40ae44a90..f64170e1c 100644 --- a/auth/tokenizer.go +++ b/auth/tokenizer.go @@ -6,7 +6,8 @@ import ( "time" "github.com/coreos/go-oidc/v3/oidc" - "github.com/dgrijalva/jwt-go" + "github.com/go-jose/go-jose/v4" + "github.com/go-jose/go-jose/v4/jwt" "github.com/golang/protobuf/ptypes/timestamp" "github.com/pkg/errors" "github.com/stackrox/infra/auth/claimrule" @@ -15,13 +16,9 @@ import ( "golang.org/x/oauth2" ) -// clockDriftLeeway is used to account for minor clock drift between our host, -// and OIDC. -// -// See this issue for context: -// https://github.com/dgrijalva/jwt-go/issues/314#issuecomment-494585527 const ( - clockDriftLeeway = int64(10 * time.Second) + // clockDriftLeeway is used to account for minor clock drift between our host and OIDC. + clockDriftLeeway = 10 * time.Second emailSuffixRedHat = "@redhat.com" ) @@ -34,7 +31,7 @@ func createHumanUser(profile oidcClaims) *v1.User { Name: profile.Name, Email: profile.Email, Picture: profile.PictureURL, - Expiry: ×tamp.Timestamp{Seconds: profile.ExpiresAt}, + Expiry: ×tamp.Timestamp{Seconds: int64(*profile.Expiry)}, } } @@ -63,24 +60,18 @@ func NewStateTokenizer(lifetime time.Duration, secret string) *stateTokenizer { // Generate generates a state JWT. func (t stateTokenizer) Generate() (string, error) { now := time.Now() - claims := jwt.StandardClaims{ - ExpiresAt: now.Add(t.lifetime).Unix(), - NotBefore: now.Unix(), - IssuedAt: now.Unix(), + nowDate := jwt.NewNumericDate(now) + claims := jwt.Claims{ + Expiry: jwt.NewNumericDate(now.Add(t.lifetime)), + NotBefore: nowDate, + IssuedAt: nowDate, } - - // Generate new token object, containing the wrapped data. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Sign and get the complete encoded token as a string using the secret - return token.SignedString(t.secret) + return signedToken(t.secret, claims) } // Validate validates a state JWT. func (t stateTokenizer) Validate(token string) error { - _, err := jwt.Parse(token, func(_ *jwt.Token) (interface{}, error) { - return t.secret, nil - }) + _, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256}) return err } @@ -109,7 +100,7 @@ func NewOidcTokenizer(verifier *oidc.IDTokenVerifier) *oidcTokenizer { // oidcClaims facilitates the unmarshalling of JWTs containing OIDC user // profile data. type oidcClaims struct { - jwt.StandardClaims + jwt.Claims FamilyName string `json:"family_name"` GivenName string `json:"given_name"` Name string `json:"name"` @@ -139,10 +130,9 @@ func (c oidcClaims) Valid() error { log.AuditLog(logging.INFO, "oidc-claim-validation", errMsg, "email", c.Email) return errors.Errorf(errMsg) default: - c.StandardClaims.IssuedAt -= clockDriftLeeway - valid := c.StandardClaims.Valid() - c.StandardClaims.IssuedAt += clockDriftLeeway - return valid + // Use an empty jwt.Expected to skip non-time-related validation and use time.Now() + // for the validation. + return c.ValidateWithLeeway(jwt.Expected{}, clockDriftLeeway) } } @@ -193,37 +183,38 @@ func NewUserTokenizer(lifetime time.Duration, secret string) *userTokenizer { // userClaims facilitates the arshalling/unmarshalling of JWTs containing v1 // .User data. type userClaims struct { + jwt.Claims User v1.User `json:"user"` - jwt.StandardClaims } // Generate generates a user JWT containing a v1.User struct. func (t userTokenizer) Generate(user *v1.User) (string, error) { now := time.Now() + nowDate := jwt.NewNumericDate(now) claims := userClaims{ User: *user, - StandardClaims: jwt.StandardClaims{ - ExpiresAt: now.Add(t.lifetime).Unix(), - NotBefore: now.Unix(), - IssuedAt: now.Unix(), + Claims: jwt.Claims{ + Expiry: jwt.NewNumericDate(now.Add(t.lifetime)), + NotBefore: nowDate, + IssuedAt: nowDate, }, } - - // Generate new token object, containing the wrapped data. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - - // Sign and get the complete encoded token as a string using the secret - return token.SignedString(t.secret) + return signedToken(t.secret, claims) } // Validate validates a user JWT and returns the contained v1.User struct. func (t userTokenizer) Validate(token string) (*v1.User, error) { + parsedToken, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256}) + if err != nil { + return nil, err + } + var claims userClaims - if _, err := jwt.ParseWithClaims(token, &claims, func(_ *jwt.Token) (interface{}, error) { - return t.secret, nil - }); err != nil { + err = parsedToken.Claims(t.secret, &claims) + if err != nil { return nil, err } + return &claims.User, nil } @@ -276,20 +267,20 @@ func (t serviceAccountTokenizer) Generate(svcacct v1.ServiceAccount) (string, er return "", errors.Wrap(err, "invalid service account") } - // Generate new token object, containing the wrapped data. - token := jwt.NewWithClaims(jwt.SigningMethodHS256, svc) - - // Sign and get the complete encoded token as a string using the secret - return token.SignedString(t.secret) + return signedToken(t.secret, svc) } // Validate validates a service account JWT and returns the contained // v1.ServiceAccount. func (t serviceAccountTokenizer) Validate(token string) (v1.ServiceAccount, error) { + parsedToken, err := jwt.ParseSigned(token, []jose.SignatureAlgorithm{jose.HS256}) + if err != nil { + return v1.ServiceAccount{}, err + } + var claims serviceAccountValidator - if _, err := jwt.ParseWithClaims(token, &claims, func(_ *jwt.Token) (interface{}, error) { - return t.secret, nil - }); err != nil { + err = parsedToken.Claims(t.secret, &claims) + if err != nil { return v1.ServiceAccount{}, err } @@ -323,3 +314,17 @@ func (t accessTokenizer) Validate(_ context.Context, rawToken *oauth2.Token) err return t.claimRules.Validate(rawAccessToken.(string)) } + +func signedToken(key []byte, claims any) (string, error) { + sigKey := jose.SigningKey{ + Algorithm: jose.HS256, + Key: key, + } + // See https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt#example-Signed for an example. + sig, err := jose.NewSigner(sigKey, (&jose.SignerOptions{}).WithType("JWT")) + if err != nil { + return "", err + } + + return jwt.Signed(sig).Claims(claims).Serialize() +} diff --git a/go.mod b/go.mod index 3aea78b10..945d6e6ec 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,8 @@ require ( github.com/argoproj/argo-workflows/v3 v3.5.5 github.com/buger/jsonparser v1.1.1 github.com/coreos/go-oidc/v3 v3.9.0 - github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 + github.com/go-jose/go-jose/v4 v4.0.2 github.com/gogo/protobuf v1.3.2 github.com/golang/protobuf v1.5.4 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 @@ -31,7 +31,6 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c google.golang.org/grpc v1.62.1 google.golang.org/protobuf v1.33.0 - gopkg.in/square/go-jose.v2 v2.6.0 k8s.io/api v0.29.2 k8s.io/apimachinery v0.29.2 k8s.io/client-go v0.29.2 diff --git a/go.sum b/go.sum index f24030957..8f244c9f6 100644 --- a/go.sum +++ b/go.sum @@ -78,7 +78,6 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= -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/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= @@ -113,6 +112,8 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ER github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= @@ -762,8 +763,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 h1:VpOs+IwYnYBaFnrNAeB8UUWtL3vEUnzSCL1nVjPhqrw= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= -gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= -gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=