From 63c0135b2b53646d442ba9880dced7e108efb37c Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 19 Mar 2021 14:38:08 -0600 Subject: [PATCH 01/16] first rough pass at go-jose Signed-off-by: Jon Carl --- go.mod | 7 ++++ go.sum | 26 +++++++++++++ validate/jose_v2/index.go | 79 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 validate/jose_v2/index.go diff --git a/go.mod b/go.mod index cb6ed17f..f6624903 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,12 @@ go 1.14 require ( github.com/form3tech-oss/jwt-go v3.2.2+incompatible + github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab github.com/google/go-cmp v0.5.5 + github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect + github.com/gorilla/mux v1.7.4 + github.com/smartystreets/assertions v1.1.0 // indirect + github.com/smartystreets/goconvey v1.6.4 + github.com/urfave/negroni v1.0.0 + gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index 11890ce3..724dedfc 100644 --- a/go.sum +++ b/go.sum @@ -4,3 +4,29 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= +github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= +github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/validate/jose_v2/index.go b/validate/jose_v2/index.go new file mode 100644 index 00000000..d95095cc --- /dev/null +++ b/validate/jose_v2/index.go @@ -0,0 +1,79 @@ +package jose_v2 + +import ( + "errors" + "fmt" + "time" + + "gopkg.in/square/go-jose.v2/jwt" +) + +type CustomClaims interface { + Validate() error +} + +type Validator struct { + // TODO(joncarl): set this to 0 by default + AllowedClockSkew time.Duration + KeyFunc func() interface{} + SigningAlgorithm string + CustomClaims func() CustomClaims + // TODO(joncarl): maybe this can be an object instead of func? + ExpectedClaims func() jwt.Expected +} + +// what should this do at a base level? +// - [X] parse +// - [X] provide key getter +// - [X] make sure signing method == alg header - maybe allow for no signing method? +// - [X] custom checks for validation +// - [X] support clock skew +// - [X] custom claims +// - [ ] maybe some function to later understand the user info from context? + +func (v *Validator) ValidateToken(token string) (interface{}, error) { + // parse + tok, err := jwt.ParseSigned(token) + if err != nil { + // TODO(joncarl): wrap the error? + return nil, err + } + + // make sure signing method == alg header + // TODO(joncarl): we need to figure out how to best handle this. + // for now we are taking the first header / signature and comparing + // the alg there. I think the simplest thing to do is not support multi + // recipient JWT. + if len(tok.Headers) == 0 { + return nil, errors.New("no headers!") + } + if v.SigningAlgorithm != "" && v.SigningAlgorithm != tok.Headers[0].Algorithm { + return nil, fmt.Errorf("expected %q signin algorithm but token specified %q", v.SigningAlgorithm, tok.Headers[0].Algorithm) + } + + // get key + jwks := v.KeyFunc() + + claimDest := []interface{}{&jwt.Claims{}} + if v.CustomClaims != nil { + claimDest = append(claimDest, v.CustomClaims()) + } + + if err = tok.Claims(jwks, claimDest...); err != nil { + return nil, fmt.Errorf("could not get token claims: %w", err) + } + + // check claims + if err = claimDest[0].(*jwt.Claims).ValidateWithLeeway(v.ExpectedClaims(), v.AllowedClockSkew); err != nil { + return nil, err + } + + // custom validation + if v.CustomClaims != nil { + if err = claimDest[1].(CustomClaims).Validate(); err != nil { + return nil, err + } + } + + return tok, nil +} From 6e4f2d12dd858bc1b89ee02468b2eb4b703dee5c Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 19 Mar 2021 16:55:34 -0600 Subject: [PATCH 02/16] add tests Signed-off-by: Jon Carl --- validate/jose_v2/doc.go | 19 +++++++ validate/jose_v2/index.go | 79 -------------------------- validate/jose_v2/jose.go | 103 ++++++++++++++++++++++++++++++++++ validate/jose_v2/jose_test.go | 97 ++++++++++++++++++++++++++++++++ 4 files changed, 219 insertions(+), 79 deletions(-) create mode 100644 validate/jose_v2/doc.go delete mode 100644 validate/jose_v2/index.go create mode 100644 validate/jose_v2/jose.go create mode 100644 validate/jose_v2/jose_test.go diff --git a/validate/jose_v2/doc.go b/validate/jose_v2/doc.go new file mode 100644 index 00000000..7df1e8cb --- /dev/null +++ b/validate/jose_v2/doc.go @@ -0,0 +1,19 @@ +/* +Package jose_v2 contains an implementation of jwtmiddleware.ValidateToken using +the Square go-jose package version 2. + +The implementation handles some nuances around JWTs and supports: +- a key func to pull the key(s) used to verify the token signature +// TODO(joncarl): maybe we should provide a high level helper func for this +- verifying the signature algorithm is what it should be +- validation of "regular" claims +- validation of custom claims +- clock skew allowances + +When this package is used, tokens are returned as `JSONWebToken` from the +gopkg.in/square/go-jose.v2/jwt package. + +Note that while the jose package does support multi-recipient JWTs, this +package does not support them. +*/ +package jose_v2 diff --git a/validate/jose_v2/index.go b/validate/jose_v2/index.go deleted file mode 100644 index d95095cc..00000000 --- a/validate/jose_v2/index.go +++ /dev/null @@ -1,79 +0,0 @@ -package jose_v2 - -import ( - "errors" - "fmt" - "time" - - "gopkg.in/square/go-jose.v2/jwt" -) - -type CustomClaims interface { - Validate() error -} - -type Validator struct { - // TODO(joncarl): set this to 0 by default - AllowedClockSkew time.Duration - KeyFunc func() interface{} - SigningAlgorithm string - CustomClaims func() CustomClaims - // TODO(joncarl): maybe this can be an object instead of func? - ExpectedClaims func() jwt.Expected -} - -// what should this do at a base level? -// - [X] parse -// - [X] provide key getter -// - [X] make sure signing method == alg header - maybe allow for no signing method? -// - [X] custom checks for validation -// - [X] support clock skew -// - [X] custom claims -// - [ ] maybe some function to later understand the user info from context? - -func (v *Validator) ValidateToken(token string) (interface{}, error) { - // parse - tok, err := jwt.ParseSigned(token) - if err != nil { - // TODO(joncarl): wrap the error? - return nil, err - } - - // make sure signing method == alg header - // TODO(joncarl): we need to figure out how to best handle this. - // for now we are taking the first header / signature and comparing - // the alg there. I think the simplest thing to do is not support multi - // recipient JWT. - if len(tok.Headers) == 0 { - return nil, errors.New("no headers!") - } - if v.SigningAlgorithm != "" && v.SigningAlgorithm != tok.Headers[0].Algorithm { - return nil, fmt.Errorf("expected %q signin algorithm but token specified %q", v.SigningAlgorithm, tok.Headers[0].Algorithm) - } - - // get key - jwks := v.KeyFunc() - - claimDest := []interface{}{&jwt.Claims{}} - if v.CustomClaims != nil { - claimDest = append(claimDest, v.CustomClaims()) - } - - if err = tok.Claims(jwks, claimDest...); err != nil { - return nil, fmt.Errorf("could not get token claims: %w", err) - } - - // check claims - if err = claimDest[0].(*jwt.Claims).ValidateWithLeeway(v.ExpectedClaims(), v.AllowedClockSkew); err != nil { - return nil, err - } - - // custom validation - if v.CustomClaims != nil { - if err = claimDest[1].(CustomClaims).Validate(); err != nil { - return nil, err - } - } - - return tok, nil -} diff --git a/validate/jose_v2/jose.go b/validate/jose_v2/jose.go new file mode 100644 index 00000000..caa89d56 --- /dev/null +++ b/validate/jose_v2/jose.go @@ -0,0 +1,103 @@ +package jose_v2 + +import ( + "fmt" + "time" + + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +type CustomClaims interface { + Validate() error +} + +type Option func(*Validator) + +func WithAllowedClockSkew(skew time.Duration) Option { + return func(v *Validator) { + v.allowedClockSkew = skew + } +} + +func WithCustomClaims(f func() CustomClaims) Option { + return func(v *Validator) { + v.customClaims = f + } +} + +func New(keyFunc func() (interface{}, error), + signatureAlgorithm jose.SignatureAlgorithm, + expectedClaims func() jwt.Expected, + opts ...Option) *Validator { + + // TODO(joncarl): error on nil keyFunc and expectedClaims as we want to + // require them + + v := &Validator{ + allowedClockSkew: 0, + keyFunc: keyFunc, + signatureAlgorithm: signatureAlgorithm, + customClaims: nil, + expectedClaims: expectedClaims, + } + + for _, opt := range opts { + opt(v) + } + + return v +} + +type Validator struct { + // required options + keyFunc func() (interface{}, error) + signatureAlgorithm jose.SignatureAlgorithm + expectedClaims func() jwt.Expected + + // optional options + allowedClockSkew time.Duration + customClaims func() CustomClaims +} + +// ValidateToken validates the passed in JWT using the jose v2 package. +func (v *Validator) ValidateToken(token string) (interface{}, error) { + tok, err := jwt.ParseSigned(token) + if err != nil { + return nil, fmt.Errorf("could not parse the token: %w", err) + } + + signatureAlgorithm := string(v.signatureAlgorithm) + + // if jwt.ParseSigned did not error there will always be at least one + // header in the token + if signatureAlgorithm != "" && signatureAlgorithm != tok.Headers[0].Algorithm { + return nil, fmt.Errorf("expected %q signin algorithm but token specified %q", signatureAlgorithm, tok.Headers[0].Algorithm) + } + + key, err := v.keyFunc() + if err != nil { + return nil, fmt.Errorf("error getting the keys from the key func: %w", err) + } + + claimDest := []interface{}{&jwt.Claims{}} + if v.customClaims != nil { + claimDest = append(claimDest, v.customClaims()) + } + + if err = tok.Claims(key, claimDest...); err != nil { + return nil, fmt.Errorf("could not get token claims: %w", err) + } + + if err = claimDest[0].(*jwt.Claims).ValidateWithLeeway(v.expectedClaims(), v.allowedClockSkew); err != nil { + return nil, fmt.Errorf("expected claims not validated: %w", err) + } + + if v.customClaims != nil { + if err = claimDest[1].(CustomClaims).Validate(); err != nil { + return nil, fmt.Errorf("custom claims not validated: %w", err) + } + } + + return tok, nil +} diff --git a/validate/jose_v2/jose_test.go b/validate/jose_v2/jose_test.go new file mode 100644 index 00000000..796dcf06 --- /dev/null +++ b/validate/jose_v2/jose_test.go @@ -0,0 +1,97 @@ +package jose_v2 + +import ( + "errors" + "testing" + + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +type testingCustomClaims struct { + Subject string + ReturnError error +} + +func (tcc *testingCustomClaims) Validate() error { + return tcc.ReturnError +} + +func Test_Validate(t *testing.T) { + testCases := []struct { + name string + signatureAlgorithm jose.SignatureAlgorithm + token string + keyFuncReturnError error + customClaims CustomClaims + expectedClaims jwt.Expected + expectedError error + }{ + { + name: "happy path", + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, + }, + { + // we want to test that when it expects RSA but we send + // HMAC encrypted with the server public key it will + // error + name: "errors on wrong algorithm", + signatureAlgorithm: jose.PS256, + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, + expectedError: errors.New("expected \"PS256\" signin algorithm but token specified \"HS256\""), + }, + { + name: "errors when jwt.ParseSigned errors", + expectedError: errors.New("could not parse the token: square/go-jose: compact JWS format must have three parts"), + }, + { + name: "errors when the key func errors", + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, + keyFuncReturnError: errors.New("key func error message"), + expectedError: errors.New("error getting the keys from the key func: key func error message"), + }, + { + name: "errors when tok.Claims errors", + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hDyICUnkCrwFJnkJHRSkwMZNSYZ9LI6z2EFJdtwFurA`, + expectedError: errors.New("could not get token claims: square/go-jose: error in cryptographic primitive"), + }, + { + name: "errors when expected claims errors", + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, + expectedClaims: jwt.Expected{Subject: "wrong subject"}, + expectedError: errors.New("expected claims not validated: square/go-jose/jwt: validation failed, invalid subject claim (sub)"), + }, + { + name: "errors when custom claims errors", + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, + customClaims: &testingCustomClaims{ReturnError: errors.New("custom claims error message")}, + expectedError: errors.New("custom claims not validated: custom claims error message"), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + var customClaimsFunc func() CustomClaims = nil + if testCase.customClaims != nil { + customClaimsFunc = func() CustomClaims { return testCase.customClaims } + } + + v := New(func() (interface{}, error) { return []byte("secret"), testCase.keyFuncReturnError }, + testCase.signatureAlgorithm, + func() jwt.Expected { return testCase.expectedClaims }, + WithCustomClaims(customClaimsFunc), + ) + _, err := v.ValidateToken(testCase.token) + if testCase.expectedError == nil && err != nil { + t.Fatalf("did not expect an error, but got %q", err.Error()) + } else if testCase.expectedError != nil { + if err == nil { + t.Fatal("expected to get an error but did not get one") + } + if testCase.expectedError.Error() != err.Error() { + t.Fatalf("did not get the expected error %q,\nbut instead got %q", testCase.expectedError.Error(), err.Error()) + } + } + }) + } +} From d8e64b681602876f36b4b82f435b2067a98ef912 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 26 Mar 2021 13:19:05 -0600 Subject: [PATCH 03/16] move jose_v2 package to josev2 to better follow conventions Signed-off-by: Jon Carl --- validate/{jose_v2 => josev2}/doc.go | 4 ++-- validate/{jose_v2 => josev2}/jose.go | 2 +- validate/{jose_v2 => josev2}/jose_test.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename validate/{jose_v2 => josev2}/doc.go (86%) rename validate/{jose_v2 => josev2}/jose.go (99%) rename validate/{jose_v2 => josev2}/jose_test.go (99%) diff --git a/validate/jose_v2/doc.go b/validate/josev2/doc.go similarity index 86% rename from validate/jose_v2/doc.go rename to validate/josev2/doc.go index 7df1e8cb..e12358b3 100644 --- a/validate/jose_v2/doc.go +++ b/validate/josev2/doc.go @@ -1,5 +1,5 @@ /* -Package jose_v2 contains an implementation of jwtmiddleware.ValidateToken using +Package josev2 contains an implementation of jwtmiddleware.ValidateToken using the Square go-jose package version 2. The implementation handles some nuances around JWTs and supports: @@ -16,4 +16,4 @@ gopkg.in/square/go-jose.v2/jwt package. Note that while the jose package does support multi-recipient JWTs, this package does not support them. */ -package jose_v2 +package josev2 diff --git a/validate/jose_v2/jose.go b/validate/josev2/jose.go similarity index 99% rename from validate/jose_v2/jose.go rename to validate/josev2/jose.go index caa89d56..c4ce78e4 100644 --- a/validate/jose_v2/jose.go +++ b/validate/josev2/jose.go @@ -1,4 +1,4 @@ -package jose_v2 +package josev2 import ( "fmt" diff --git a/validate/jose_v2/jose_test.go b/validate/josev2/jose_test.go similarity index 99% rename from validate/jose_v2/jose_test.go rename to validate/josev2/jose_test.go index 796dcf06..53ab394f 100644 --- a/validate/jose_v2/jose_test.go +++ b/validate/josev2/jose_test.go @@ -1,4 +1,4 @@ -package jose_v2 +package josev2 import ( "errors" From 6aa74312ed5043cd457ac1c19482e566770a4fa2 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 26 Mar 2021 17:41:07 -0600 Subject: [PATCH 04/16] add more examples and change up the returned object by josev2 Signed-off-by: Jon Carl --- go.sum | 3 + jwtmiddleware.go | 1 + validate/josev2/examples/README.md | 85 ++++++++++++++++++ validate/josev2/examples/main.go | 87 +++++++++++++++++++ validate/josev2/{jose.go => josev2.go} | 55 ++++++++++-- .../josev2/{jose_test.go => josev2_test.go} | 51 +++++++---- 6 files changed, 255 insertions(+), 27 deletions(-) create mode 100644 validate/josev2/examples/README.md create mode 100644 validate/josev2/examples/main.go rename validate/josev2/{jose.go => josev2.go} (52%) rename validate/josev2/{jose_test.go => josev2_test.go} (62%) diff --git a/go.sum b/go.sum index 724dedfc..c13fca3e 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IV golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= @@ -28,5 +30,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= diff --git a/jwtmiddleware.go b/jwtmiddleware.go index 9c0a0ca2..c50df96f 100644 --- a/jwtmiddleware.go +++ b/jwtmiddleware.go @@ -62,6 +62,7 @@ type TokenExtractor func(r *http.Request) (string, error) // error message describing why validation failed. // Inside of ValidateToken is where things like key and alg checking can // happen. In the default implementation we can add safe defaults for those. +// TODO: should take context? type ValidateToken func(string) (interface{}, error) type JWTMiddleware struct { diff --git a/validate/josev2/examples/README.md b/validate/josev2/examples/README.md new file mode 100644 index 00000000..6bcee7d4 --- /dev/null +++ b/validate/josev2/examples/README.md @@ -0,0 +1,85 @@ +# josev2 examples + +These examples should get you up and running and understanding how to best use +the validator. + +You will need `jwt-cli` to work through the examples: +``` +npm install --global "@clarketm/jwt-cli" +``` + +In in terminal, run the example to get started: +``` +go run main.go +``` +Now you can follow the examples below. + +### with clockskew +The example allows clock skew of 30 seconds. Let's use a token that expired 45 +seconds ago to show that it will reject this. +``` +export TOKEN=$(jwt sign -n "{\"iat\":$(date -r $(( $(date +%s) - 3645 )) +%s),\"iss\":\"josev2-example\"}" "secret") +curl "127.0.0.1:3000" -H "Authorization: Bearer $TOKEN" +``` + +Now lets generate a token that expired 15 seconds ago and watch as it is not +rejected. +``` +export TOKEN=$(jwt sign -n "{\"iat\":$(date -r $(( $(date +%s) - 3615 )) +%s),\"iss\":\"josev2-example\"}" "secret") +curl "127.0.0.1:3000" -H "Authorization: Bearer $TOKEN" +``` + +### custom claims +We can use custom claims in our token and have the validator pass them back to +us in the user context. When the endpoint responds after a valid request it +prints out the CustomClaims. Let's add two claims to our token to see that it +handles the claim we have defined in CustomClaimsExample but does nothing with +the claim we do not have defined. +``` +export TOKEN=$(jwt sign -n "{\"username\":\"user123\",\"hairColor\":\"brown\",\"iss\":\"josev2-example\"}" "secret") +curl "127.0.0.1:3000" -H "Authorization: Bearer $TOKEN" +``` +It will print out something like +```json +{ + "CustomClaims": { + "username": "user123" + }, + "Claims": { + "iss": "josev2-example", + "exp": 1616801896, + "iat": 1616798296 + } +} +``` +As you can see the `username` claim is there, but the `hairColor` claim is not. + +### custom validaton +Along with custom claims we can also run custom validation logic to determine +if the token should be rejected or not. Our example is setup to reject anything +that has the field `shouldReject` set to `true`. +``` +export TOKEN=$(jwt sign -n "{\"shouldReject\":true,\"iss\":\"josev2-example\"}" "secret") +curl "127.0.0.1:3000" -H "Authorization: Bearer $TOKEN" +``` +It will print out something like +``` +The token isn't valid: custom claims not validated: should reject was set to true +``` +The message comes directly from the custom validation! + +### expected claims +In all of the above examples we've seen the `iss` field being set. That's +because it expects the issuer to be `josev2-example`. This validation is built +right into jose. If we remove the field it will error on that field. +``` +export TOKEN=$(jwt sign -n "{}" "secret") +curl "127.0.0.1:3000" -H "Authorization: Bearer $TOKEN" +``` +It will print out something like +``` +The token isn't valid: expected claims not validated: square/go-jose/jwt: validation failed, invalid issuer claim (iss) +``` + + +Take a look through the example code and things will make a lot more sense. diff --git a/validate/josev2/examples/main.go b/validate/josev2/examples/main.go new file mode 100644 index 00000000..f4c75543 --- /dev/null +++ b/validate/josev2/examples/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "time" + + jwtmiddleware "github.com/auth0/go-jwt-middleware" + "github.com/auth0/go-jwt-middleware/validate/josev2" + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) + +// CustomClaimsExample contains custom data we want from the token. +type CustomClaimsExample struct { + Username string `json:"username"` + ShouldReject bool `json:"shouldReject,omitempty"` +} + +// Validate does nothing for this example +func (c *CustomClaimsExample) Validate() error { + if c.ShouldReject { + return errors.New("should reject was set to true") + } + return nil +} + +var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value("user") + j, err := json.MarshalIndent(user, "", "\t") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Println(err) + } + + fmt.Fprintf(w, "This is an authenticated request") + fmt.Fprintf(w, "Claim content:\n") + fmt.Fprint(w, string(j)) +}) + +func main() { + keyFunc := func() (interface{}, error) { + // our token must be signed using this data + return []byte("secret"), nil + } + expectedClaims := func() jwt.Expected { + // By setting up expected claims we are saying a token must + // have the data we specify. + return jwt.Expected{ + Issuer: "josev2-example", + Time: time.Now(), + } + } + customClaims := func() josev2.CustomClaims { + // we want this struct to be filled in with our custom claims + // from the token + return &CustomClaimsExample{} + } + + // setup the josev2 validator + validator := josev2.New( + keyFunc, + jose.HS256, + josev2.WithExpectedClaims(expectedClaims), + josev2.WithCustomClaims(customClaims), + josev2.WithAllowedClockSkew(30*time.Second), + ) + + // setup the middleware + m := jwtmiddleware.New(jwtmiddleware.Options{ + Validate: validator.ValidateToken, + }) + + app := m.Handler(handler) + http.ListenAndServe("0.0.0.0:3000", app) + // try it out with eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb3NldjItZXhhbXBsZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoidXNlcjEyMyJ9.1v7S4aF7lVM92bRZ8tVTrKGZ6FwkX-7ybZQA5A7mq8E + // which is signed with 'secret' and has the data: + // { + // "iss": "josev2-example", + // "sub": "1234567890", + // "name": "John Doe", + // "iat": 1516239022, + // "username": "user123" + // } +} diff --git a/validate/josev2/jose.go b/validate/josev2/josev2.go similarity index 52% rename from validate/josev2/jose.go rename to validate/josev2/josev2.go index c4ce78e4..8c3f28a3 100644 --- a/validate/josev2/jose.go +++ b/validate/josev2/josev2.go @@ -8,38 +8,70 @@ import ( "gopkg.in/square/go-jose.v2/jwt" ) +// CustomClaims defines any custom data / claims wanted. The validator will +// call the Validate function which is where custom validation logic can be +// defined. type CustomClaims interface { Validate() error } +// UserContext is the struct that will be inserted into the context for the +// user. CustomClaims will be nil unless WithCustomClaims we passed to New. +type UserContext struct { + CustomClaims CustomClaims + Claims jwt.Claims +} + +// Option is how options for the validator are setup. type Option func(*Validator) +// WithAllowedClockSkew is an option which sets up the allowed clock skew for +// the token. Note that in order to use this the expected claims Time field +// MUST not be time.IsZero(). If this option is not used clock skew is not +// allowed. func WithAllowedClockSkew(skew time.Duration) Option { return func(v *Validator) { v.allowedClockSkew = skew } } +// WithCustomClaims sets up a function that returns the object CustomClaims are +// unmarshalled into and the object which Validate is called on for custom +// validation. If this option is not used the validator will do nothing for +// custom claims. func WithCustomClaims(f func() CustomClaims) Option { return func(v *Validator) { v.customClaims = f } } +// WithExpectedClaims sets up a function that returns the object used to +// validate claims. If this option is not used a default jwt.Expected object is +// used which only validates token time. +func WithExpectedClaims(f func() jwt.Expected) Option { + return func(v *Validator) { + v.expectedClaims = f + } +} + +// New sets up a new Validator. With the required keyFunc and +// signatureAlgorithm as well as options. func New(keyFunc func() (interface{}, error), signatureAlgorithm jose.SignatureAlgorithm, - expectedClaims func() jwt.Expected, opts ...Option) *Validator { - // TODO(joncarl): error on nil keyFunc and expectedClaims as we want to - // require them + // TODO(joncarl): error on nil keyFunc as we want to require it v := &Validator{ allowedClockSkew: 0, keyFunc: keyFunc, signatureAlgorithm: signatureAlgorithm, customClaims: nil, - expectedClaims: expectedClaims, + expectedClaims: func() jwt.Expected { + return jwt.Expected{ + Time: time.Now(), + } + }, } for _, opt := range opts { @@ -53,9 +85,9 @@ type Validator struct { // required options keyFunc func() (interface{}, error) signatureAlgorithm jose.SignatureAlgorithm - expectedClaims func() jwt.Expected - // optional options + // optional options which we will default if not specified + expectedClaims func() jwt.Expected allowedClockSkew time.Duration customClaims func() CustomClaims } @@ -89,15 +121,20 @@ func (v *Validator) ValidateToken(token string) (interface{}, error) { return nil, fmt.Errorf("could not get token claims: %w", err) } - if err = claimDest[0].(*jwt.Claims).ValidateWithLeeway(v.expectedClaims(), v.allowedClockSkew); err != nil { + userCtx := &UserContext{ + Claims: *claimDest[0].(*jwt.Claims), + } + + if err = userCtx.Claims.ValidateWithLeeway(v.expectedClaims(), v.allowedClockSkew); err != nil { return nil, fmt.Errorf("expected claims not validated: %w", err) } if v.customClaims != nil { - if err = claimDest[1].(CustomClaims).Validate(); err != nil { + userCtx.CustomClaims = claimDest[1].(CustomClaims) + if err = userCtx.CustomClaims.Validate(); err != nil { return nil, fmt.Errorf("custom claims not validated: %w", err) } } - return tok, nil + return userCtx, nil } diff --git a/validate/josev2/jose_test.go b/validate/josev2/josev2_test.go similarity index 62% rename from validate/josev2/jose_test.go rename to validate/josev2/josev2_test.go index 53ab394f..89e38e8d 100644 --- a/validate/josev2/jose_test.go +++ b/validate/josev2/josev2_test.go @@ -4,6 +4,7 @@ import ( "errors" "testing" + "github.com/google/go-cmp/cmp" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -17,6 +18,13 @@ func (tcc *testingCustomClaims) Validate() error { return tcc.ReturnError } +func equalErrors(actual error, expected string) bool { + if actual == nil { + return expected == "" + } + return actual.Error() == expected +} + func Test_Validate(t *testing.T) { testCases := []struct { name string @@ -25,11 +33,15 @@ func Test_Validate(t *testing.T) { keyFuncReturnError error customClaims CustomClaims expectedClaims jwt.Expected - expectedError error + expectedError string + expectedContext *UserContext }{ { name: "happy path", - token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, + token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.Rq8IxqeX7eA6GgYxlcHdPFVRNFFZc5rEI3MQTZZbK3I`, + expectedContext: &UserContext{ + Claims: jwt.Claims{Subject: "1234567890"}, + }, }, { // we want to test that when it expects RSA but we send @@ -38,34 +50,34 @@ func Test_Validate(t *testing.T) { name: "errors on wrong algorithm", signatureAlgorithm: jose.PS256, token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, - expectedError: errors.New("expected \"PS256\" signin algorithm but token specified \"HS256\""), + expectedError: "expected \"PS256\" signin algorithm but token specified \"HS256\"", }, { name: "errors when jwt.ParseSigned errors", - expectedError: errors.New("could not parse the token: square/go-jose: compact JWS format must have three parts"), + expectedError: "could not parse the token: square/go-jose: compact JWS format must have three parts", }, { name: "errors when the key func errors", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, keyFuncReturnError: errors.New("key func error message"), - expectedError: errors.New("error getting the keys from the key func: key func error message"), + expectedError: "error getting the keys from the key func: key func error message", }, { name: "errors when tok.Claims errors", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.hDyICUnkCrwFJnkJHRSkwMZNSYZ9LI6z2EFJdtwFurA`, - expectedError: errors.New("could not get token claims: square/go-jose: error in cryptographic primitive"), + expectedError: "could not get token claims: square/go-jose: error in cryptographic primitive", }, { name: "errors when expected claims errors", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, expectedClaims: jwt.Expected{Subject: "wrong subject"}, - expectedError: errors.New("expected claims not validated: square/go-jose/jwt: validation failed, invalid subject claim (sub)"), + expectedError: "expected claims not validated: square/go-jose/jwt: validation failed, invalid subject claim (sub)", }, { name: "errors when custom claims errors", token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, customClaims: &testingCustomClaims{ReturnError: errors.New("custom claims error message")}, - expectedError: errors.New("custom claims not validated: custom claims error message"), + expectedError: "custom claims not validated: custom claims error message", }, } @@ -78,20 +90,23 @@ func Test_Validate(t *testing.T) { v := New(func() (interface{}, error) { return []byte("secret"), testCase.keyFuncReturnError }, testCase.signatureAlgorithm, - func() jwt.Expected { return testCase.expectedClaims }, + WithExpectedClaims(func() jwt.Expected { return testCase.expectedClaims }), WithCustomClaims(customClaimsFunc), ) - _, err := v.ValidateToken(testCase.token) - if testCase.expectedError == nil && err != nil { - t.Fatalf("did not expect an error, but got %q", err.Error()) - } else if testCase.expectedError != nil { - if err == nil { - t.Fatal("expected to get an error but did not get one") - } - if testCase.expectedError.Error() != err.Error() { - t.Fatalf("did not get the expected error %q,\nbut instead got %q", testCase.expectedError.Error(), err.Error()) + actualContext, err := v.ValidateToken(testCase.token) + if !equalErrors(err, testCase.expectedError) { + t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", testCase.expectedError, err) + } + + if (testCase.expectedContext == nil && actualContext != nil) || (testCase.expectedContext != nil && actualContext == nil) { + t.Fatalf("wanted user context:\n%+v\ngot:\n%+v\n", testCase.expectedContext, actualContext) + } else if testCase.expectedContext != nil { + if diff := cmp.Diff(testCase.expectedContext, actualContext.(*UserContext)); diff != "" { + t.Errorf("user context mismatch (-want +got):\n%s", diff) } + } + }) } } From f9271da2978a2e5e61f533806741b835b482adf9 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 2 Apr 2021 14:47:40 -0600 Subject: [PATCH 05/16] add tests for New and add context passing to ValidateToken Signed-off-by: Jon Carl --- jwtmiddleware.go | 3 +- validate/josev2/doc.go | 2 +- validate/josev2/examples/main.go | 12 ++++++-- validate/josev2/josev2.go | 24 ++++++++------- validate/josev2/josev2_test.go | 50 ++++++++++++++++++++++++++++++-- 5 files changed, 72 insertions(+), 19 deletions(-) diff --git a/jwtmiddleware.go b/jwtmiddleware.go index c50df96f..f0d0c8fb 100644 --- a/jwtmiddleware.go +++ b/jwtmiddleware.go @@ -62,8 +62,7 @@ type TokenExtractor func(r *http.Request) (string, error) // error message describing why validation failed. // Inside of ValidateToken is where things like key and alg checking can // happen. In the default implementation we can add safe defaults for those. -// TODO: should take context? -type ValidateToken func(string) (interface{}, error) +type ValidateToken func(context.Context, string) (interface{}, error) type JWTMiddleware struct { validateToken ValidateToken diff --git a/validate/josev2/doc.go b/validate/josev2/doc.go index e12358b3..5dd634f7 100644 --- a/validate/josev2/doc.go +++ b/validate/josev2/doc.go @@ -4,7 +4,7 @@ the Square go-jose package version 2. The implementation handles some nuances around JWTs and supports: - a key func to pull the key(s) used to verify the token signature -// TODO(joncarl): maybe we should provide a high level helper func for this +// TODO(joncarl): maybe we should provide a high level helper func for the above - verifying the signature algorithm is what it should be - validation of "regular" claims - validation of custom claims diff --git a/validate/josev2/examples/main.go b/validate/josev2/examples/main.go index f4c75543..8fa009dc 100644 --- a/validate/josev2/examples/main.go +++ b/validate/josev2/examples/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "errors" "fmt" @@ -20,7 +21,7 @@ type CustomClaimsExample struct { } // Validate does nothing for this example -func (c *CustomClaimsExample) Validate() error { +func (c *CustomClaimsExample) Validate(ctx context.Context) error { if c.ShouldReject { return errors.New("should reject was set to true") } @@ -41,7 +42,7 @@ var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { }) func main() { - keyFunc := func() (interface{}, error) { + keyFunc := func(ctx context.Context) (interface{}, error) { // our token must be signed using this data return []byte("secret"), nil } @@ -60,7 +61,7 @@ func main() { } // setup the josev2 validator - validator := josev2.New( + validator, err := josev2.New( keyFunc, jose.HS256, josev2.WithExpectedClaims(expectedClaims), @@ -68,6 +69,11 @@ func main() { josev2.WithAllowedClockSkew(30*time.Second), ) + if err != nil { + // we'll panic in order to fail fast + panic(err) + } + // setup the middleware m := jwtmiddleware.New(jwtmiddleware.Options{ Validate: validator.ValidateToken, diff --git a/validate/josev2/josev2.go b/validate/josev2/josev2.go index 8c3f28a3..3dc9b429 100644 --- a/validate/josev2/josev2.go +++ b/validate/josev2/josev2.go @@ -1,6 +1,8 @@ package josev2 import ( + "context" + "errors" "fmt" "time" @@ -12,11 +14,11 @@ import ( // call the Validate function which is where custom validation logic can be // defined. type CustomClaims interface { - Validate() error + Validate(context.Context) error } // UserContext is the struct that will be inserted into the context for the -// user. CustomClaims will be nil unless WithCustomClaims we passed to New. +// user. CustomClaims will be nil unless WithCustomClaims is passed to New. type UserContext struct { CustomClaims CustomClaims Claims jwt.Claims @@ -56,11 +58,13 @@ func WithExpectedClaims(f func() jwt.Expected) Option { // New sets up a new Validator. With the required keyFunc and // signatureAlgorithm as well as options. -func New(keyFunc func() (interface{}, error), +func New(keyFunc func(context.Context) (interface{}, error), signatureAlgorithm jose.SignatureAlgorithm, - opts ...Option) *Validator { + opts ...Option) (*Validator, error) { - // TODO(joncarl): error on nil keyFunc as we want to require it + if keyFunc == nil { + return nil, errors.New("keyFunc is required but was nil") + } v := &Validator{ allowedClockSkew: 0, @@ -78,12 +82,12 @@ func New(keyFunc func() (interface{}, error), opt(v) } - return v + return v, nil } type Validator struct { // required options - keyFunc func() (interface{}, error) + keyFunc func(context.Context) (interface{}, error) signatureAlgorithm jose.SignatureAlgorithm // optional options which we will default if not specified @@ -93,7 +97,7 @@ type Validator struct { } // ValidateToken validates the passed in JWT using the jose v2 package. -func (v *Validator) ValidateToken(token string) (interface{}, error) { +func (v *Validator) ValidateToken(ctx context.Context, token string) (interface{}, error) { tok, err := jwt.ParseSigned(token) if err != nil { return nil, fmt.Errorf("could not parse the token: %w", err) @@ -107,7 +111,7 @@ func (v *Validator) ValidateToken(token string) (interface{}, error) { return nil, fmt.Errorf("expected %q signin algorithm but token specified %q", signatureAlgorithm, tok.Headers[0].Algorithm) } - key, err := v.keyFunc() + key, err := v.keyFunc(ctx) if err != nil { return nil, fmt.Errorf("error getting the keys from the key func: %w", err) } @@ -131,7 +135,7 @@ func (v *Validator) ValidateToken(token string) (interface{}, error) { if v.customClaims != nil { userCtx.CustomClaims = claimDest[1].(CustomClaims) - if err = userCtx.CustomClaims.Validate(); err != nil { + if err = userCtx.CustomClaims.Validate(ctx); err != nil { return nil, fmt.Errorf("custom claims not validated: %w", err) } } diff --git a/validate/josev2/josev2_test.go b/validate/josev2/josev2_test.go index 89e38e8d..864a2d38 100644 --- a/validate/josev2/josev2_test.go +++ b/validate/josev2/josev2_test.go @@ -1,6 +1,7 @@ package josev2 import ( + "context" "errors" "testing" @@ -14,7 +15,7 @@ type testingCustomClaims struct { ReturnError error } -func (tcc *testingCustomClaims) Validate() error { +func (tcc *testingCustomClaims) Validate(ctx context.Context) error { return tcc.ReturnError } @@ -88,12 +89,12 @@ func Test_Validate(t *testing.T) { customClaimsFunc = func() CustomClaims { return testCase.customClaims } } - v := New(func() (interface{}, error) { return []byte("secret"), testCase.keyFuncReturnError }, + v, _ := New(func(ctx context.Context) (interface{}, error) { return []byte("secret"), testCase.keyFuncReturnError }, testCase.signatureAlgorithm, WithExpectedClaims(func() jwt.Expected { return testCase.expectedClaims }), WithCustomClaims(customClaimsFunc), ) - actualContext, err := v.ValidateToken(testCase.token) + actualContext, err := v.ValidateToken(context.Background(), testCase.token) if !equalErrors(err, testCase.expectedError) { t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", testCase.expectedError, err) } @@ -110,3 +111,46 @@ func Test_Validate(t *testing.T) { }) } } + +func Test_New(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + keyFunc := func(ctx context.Context) (interface{}, error) { return nil, nil } + customClaims := func() CustomClaims { return nil } + + v, err := New(keyFunc, jose.HS256, WithCustomClaims(customClaims)) + + if !equalErrors(err, "") { + t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", "", err) + } + + if v.allowedClockSkew != 0 { + t.Logf("expected allowedClockSkew to be 0 but it was %d", v.allowedClockSkew) + t.Fail() + } + + if v.keyFunc == nil { + t.Log("keyFunc was nil when it should not have been") + t.Fail() + } + + if v.signatureAlgorithm != jose.HS256 { + t.Logf("signatureAlgorithm was %q when it should have been %q", v.signatureAlgorithm, jose.HS256) + t.Fail() + } + + if v.customClaims == nil { + t.Log("customClaims was nil when it should not have been") + t.Fail() + } + }) + + t.Run("error on no keyFunc", func(t *testing.T) { + _, err := New(nil, jose.HS256) + + expectedErr := "keyFunc is required but was nil" + if !equalErrors(err, expectedErr) { + t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", expectedErr, err) + } + }) + +} From 71a966efa70a599cb2c312353c38aace001c8463 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 23 Apr 2021 11:26:49 -0600 Subject: [PATCH 06/16] add a comment on keyFunc to explain it better Signed-off-by: Jon Carl --- validate/josev2/josev2.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/validate/josev2/josev2.go b/validate/josev2/josev2.go index 3dc9b429..4e766097 100644 --- a/validate/josev2/josev2.go +++ b/validate/josev2/josev2.go @@ -87,6 +87,13 @@ func New(keyFunc func(context.Context) (interface{}, error), type Validator struct { // required options + + // in the past keyFunc might take in a token as a parameter in order to + // allow the function provider to return a key based on a header kid. + // With josev2 `jose.JSONWebKeySet` is supported as a return type of + // this function which hands off the heavy lifting of determining which + // key to used based on the header `kid` to the josev2 library. + // TODO(joncarl): provide an example of using a kid keyFunc func(context.Context) (interface{}, error) signatureAlgorithm jose.SignatureAlgorithm From 6288cff9070511a9169757e22c8c73b0ae073d4e Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 15:02:38 -0600 Subject: [PATCH 07/16] fix issues after rebasing v2 in Signed-off-by: Jon Carl --- examples/http-example/main.go | 3 ++- jwtmiddleware.go | 4 ++-- jwtmiddleware_test.go | 17 +++++++++-------- validate/josev2/examples/main.go | 7 ++----- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/examples/http-example/main.go b/examples/http-example/main.go index a1a18efc..5b3b74cb 100644 --- a/examples/http-example/main.go +++ b/examples/http-example/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "net/http" @@ -9,7 +10,7 @@ import ( ) // TODO: replace this with default validate token func once it is merged in -func REPLACE_ValidateToken(token string) (interface{}, error) { +func REPLACE_ValidateToken(_ context.Context, token string) (interface{}, error) { // Now parse the token parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { return []byte("My Secret"), nil diff --git a/jwtmiddleware.go b/jwtmiddleware.go index f0d0c8fb..7a95246c 100644 --- a/jwtmiddleware.go +++ b/jwtmiddleware.go @@ -123,7 +123,7 @@ func WithValidateOnOptions(value bool) Option { // New constructs a new JWTMiddleware instance with the supplied options. func New(opts ...Option) *JWTMiddleware { m := &JWTMiddleware{ - validateToken: func(string) (interface{}, error) { panic("not implemented") }, + validateToken: func(context.Context, string) (interface{}, error) { panic("not implemented") }, errorHandler: DefaultErrorHandler, credentialsOptional: false, tokenExtractor: AuthHeaderTokenExtractor, @@ -228,7 +228,7 @@ func (m *JWTMiddleware) CheckJWT(next http.Handler) http.Handler { } // validate the token using the token validator - validToken, err := m.validateToken(token) + validToken, err := m.validateToken(r.Context(), token) if err != nil { m.errorHandler(w, r, &invalidError{details: err}) return diff --git a/jwtmiddleware_test.go b/jwtmiddleware_test.go index 1012bd13..ece15f5f 100644 --- a/jwtmiddleware_test.go +++ b/jwtmiddleware_test.go @@ -1,6 +1,7 @@ package jwtmiddleware import ( + "context" "errors" "fmt" "io/ioutil" @@ -27,7 +28,7 @@ func Test_defaults(t *testing.T) { }{ { name: "happy path", - options: []Option{WithValidateToken(func(token string) (interface{}, error) { + options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil })}, token: "bearer abc", @@ -37,7 +38,7 @@ func Test_defaults(t *testing.T) { }, { name: "validate on options", - options: []Option{WithValidateToken(func(token string) (interface{}, error) { + options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil })}, method: http.MethodOptions, @@ -48,7 +49,7 @@ func Test_defaults(t *testing.T) { }, { name: "bad token format", - options: []Option{WithValidateToken(func(token string) (interface{}, error) { + options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil })}, token: "abc", @@ -56,7 +57,7 @@ func Test_defaults(t *testing.T) { }, { name: "credentials not optional", - options: []Option{WithValidateToken(func(token string) (interface{}, error) { + options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil })}, token: "", @@ -64,7 +65,7 @@ func Test_defaults(t *testing.T) { }, { name: "validate token errors", - options: []Option{WithValidateToken(func(token string) (interface{}, error) { + options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return nil, errors.New("validate token error") })}, token: "bearer abc", @@ -74,7 +75,7 @@ func Test_defaults(t *testing.T) { name: "validateOnOptions set to false", options: []Option{ WithValidateOnOptions(false), - WithValidateToken(func(token string) (interface{}, error) { + WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return nil, errors.New("should not hit me since we are not validating on options") }), }, @@ -97,7 +98,7 @@ func Test_defaults(t *testing.T) { WithTokenExtractor(func(r *http.Request) (string, error) { return "", nil }), - WithValidateToken(func(token string) (interface{}, error) { + WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return nil, errors.New("should not hit me since credentials are optional and there are none") }), }, @@ -111,7 +112,7 @@ func Test_defaults(t *testing.T) { WithTokenExtractor(func(r *http.Request) (string, error) { return "", nil }), - WithValidateToken(func(token string) (interface{}, error) { + WithValidateToken(func(_ context.Context, token string) (interface{}, error) { return nil, errors.New("should not hit me since ErrJWTMissing should be returned") }), }, diff --git a/validate/josev2/examples/main.go b/validate/josev2/examples/main.go index 8fa009dc..e90eec18 100644 --- a/validate/josev2/examples/main.go +++ b/validate/josev2/examples/main.go @@ -75,12 +75,9 @@ func main() { } // setup the middleware - m := jwtmiddleware.New(jwtmiddleware.Options{ - Validate: validator.ValidateToken, - }) + m := jwtmiddleware.New(jwtmiddleware.WithValidateToken(validator.ValidateToken)) - app := m.Handler(handler) - http.ListenAndServe("0.0.0.0:3000", app) + http.ListenAndServe("0.0.0.0:3000", m.CheckJWT(handler)) // try it out with eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb3NldjItZXhhbXBsZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoidXNlcjEyMyJ9.1v7S4aF7lVM92bRZ8tVTrKGZ6FwkX-7ybZQA5A7mq8E // which is signed with 'secret' and has the data: // { From 65f7e80242bc2aefce03ad221b95a6498b1c115b Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 15:13:45 -0600 Subject: [PATCH 08/16] require validate token to be passed to the middleware New func Signed-off-by: Jon Carl --- examples/http-example/main.go | 2 +- jwtmiddleware.go | 8 +++-- jwtmiddleware_test.go | 52 +++++++++++++++++--------------- validate/josev2/examples/main.go | 2 +- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/examples/http-example/main.go b/examples/http-example/main.go index 5b3b74cb..402f64b3 100644 --- a/examples/http-example/main.go +++ b/examples/http-example/main.go @@ -39,7 +39,7 @@ var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { }) func main() { - jwtMiddleware := jwtmiddleware.New(jwtmiddleware.WithValidateToken(REPLACE_ValidateToken)) + jwtMiddleware := jwtmiddleware.New(REPLACE_ValidateToken) http.ListenAndServe("0.0.0.0:3000", jwtMiddleware.CheckJWT(myHandler)) } diff --git a/jwtmiddleware.go b/jwtmiddleware.go index 7a95246c..846b36b0 100644 --- a/jwtmiddleware.go +++ b/jwtmiddleware.go @@ -120,10 +120,12 @@ func WithValidateOnOptions(value bool) Option { } } -// New constructs a new JWTMiddleware instance with the supplied options. -func New(opts ...Option) *JWTMiddleware { +// New constructs a new JWTMiddleware instance with the supplied options. It +// requires a ValidateToken function to be passed in so it can properly +// validate tokens. +func New(validateToken ValidateToken, opts ...Option) *JWTMiddleware { m := &JWTMiddleware{ - validateToken: func(context.Context, string) (interface{}, error) { panic("not implemented") }, + validateToken: validateToken, errorHandler: DefaultErrorHandler, credentialsOptional: false, tokenExtractor: AuthHeaderTokenExtractor, diff --git a/jwtmiddleware_test.go b/jwtmiddleware_test.go index ece15f5f..fa57664b 100644 --- a/jwtmiddleware_test.go +++ b/jwtmiddleware_test.go @@ -17,10 +17,11 @@ import ( // TODO(joncarl): replace with actual JWTs once we have the validate stuff plumbed in func Test_defaults(t *testing.T) { tests := []struct { - name string - options []Option - method string - token string + name string + validateToken ValidateToken + options []Option + method string + token string wantToken map[string]string wantStatusCode int @@ -28,9 +29,9 @@ func Test_defaults(t *testing.T) { }{ { name: "happy path", - options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { + validateToken: func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil - })}, + }, token: "bearer abc", wantToken: map[string]string{"foo": "bar"}, wantStatusCode: http.StatusOK, @@ -38,9 +39,9 @@ func Test_defaults(t *testing.T) { }, { name: "validate on options", - options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { + validateToken: func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil - })}, + }, method: http.MethodOptions, token: "bearer abc", wantToken: map[string]string{"foo": "bar"}, @@ -49,35 +50,35 @@ func Test_defaults(t *testing.T) { }, { name: "bad token format", - options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { + validateToken: func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil - })}, + }, token: "abc", wantStatusCode: http.StatusInternalServerError, }, { name: "credentials not optional", - options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { + validateToken: func(_ context.Context, token string) (interface{}, error) { return map[string]string{"foo": "bar"}, nil - })}, + }, token: "", wantStatusCode: http.StatusBadRequest, }, { name: "validate token errors", - options: []Option{WithValidateToken(func(_ context.Context, token string) (interface{}, error) { + validateToken: func(_ context.Context, token string) (interface{}, error) { return nil, errors.New("validate token error") - })}, + }, token: "bearer abc", wantStatusCode: http.StatusUnauthorized, }, { name: "validateOnOptions set to false", + validateToken: func(_ context.Context, token string) (interface{}, error) { + return nil, errors.New("should not hit me since we are not validating on options") + }, options: []Option{ WithValidateOnOptions(false), - WithValidateToken(func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since we are not validating on options") - }), }, method: http.MethodOptions, token: "bearer abc", @@ -86,6 +87,9 @@ func Test_defaults(t *testing.T) { }, { name: "tokenExtractor errors", + validateToken: func(_ context.Context, token string) (interface{}, error) { + return nil, errors.New("should not hit me since we are erroring on token extraction") + }, options: []Option{WithTokenExtractor(func(r *http.Request) (string, error) { return "", errors.New("token extractor error") })}, @@ -93,28 +97,28 @@ func Test_defaults(t *testing.T) { }, { name: "credentialsOptional true", + validateToken: func(_ context.Context, token string) (interface{}, error) { + return nil, errors.New("should not hit me since credentials are optional and there are none") + }, options: []Option{ WithCredentialsOptional(true), WithTokenExtractor(func(r *http.Request) (string, error) { return "", nil }), - WithValidateToken(func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since credentials are optional and there are none") - }), }, wantStatusCode: http.StatusOK, wantBody: "authenticated", }, { name: "credentialsOptional false", + validateToken: func(_ context.Context, token string) (interface{}, error) { + return nil, errors.New("should not hit me since ErrJWTMissing should be returned") + }, options: []Option{ WithCredentialsOptional(false), WithTokenExtractor(func(r *http.Request) (string, error) { return "", nil }), - WithValidateToken(func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since ErrJWTMissing should be returned") - }), }, wantStatusCode: http.StatusBadRequest, }, @@ -128,7 +132,7 @@ func Test_defaults(t *testing.T) { tc.method = http.MethodGet } - m := New(tc.options...) + m := New(tc.validateToken, tc.options...) ts := httptest.NewServer(m.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if ctxToken, ok := r.Context().Value(ContextKey{}).(map[string]string); ok { actualContextToken = ctxToken diff --git a/validate/josev2/examples/main.go b/validate/josev2/examples/main.go index e90eec18..3531414e 100644 --- a/validate/josev2/examples/main.go +++ b/validate/josev2/examples/main.go @@ -75,7 +75,7 @@ func main() { } // setup the middleware - m := jwtmiddleware.New(jwtmiddleware.WithValidateToken(validator.ValidateToken)) + m := jwtmiddleware.New(validator.ValidateToken) http.ListenAndServe("0.0.0.0:3000", m.CheckJWT(handler)) // try it out with eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb3NldjItZXhhbXBsZSIsInN1YiI6IjEyMzQ1Njc4OTAiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsInVzZXJuYW1lIjoidXNlcjEyMyJ9.1v7S4aF7lVM92bRZ8tVTrKGZ6FwkX-7ybZQA5A7mq8E From 16a2361e4e61c3a7cde40746597f6115278b1b4e Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 15:59:42 -0600 Subject: [PATCH 09/16] update the README Signed-off-by: Jon Carl --- README.md | 110 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 97 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index af31e8eb..3c592a0b 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,106 @@ # GO JWT Middleware +[![GoDoc Widget]][GoDoc] + **WARNING** This `v2` branch is not production ready - use at your own risk. -TODO: update this README in the `v2` branch. We're waiting so as not to hold everything up in the testing branch. Also some of the default validation logic needs to be added here. +Golang middleware to check and validate [JWTs](jwt.io) in the request and add the valid token contents to the request context. + +## Installation +``` +go get github.com/auth0/go-jwt-middleware +``` + +## Usage +```golang +package main + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + + jwtmiddleware "github.com/auth0/go-jwt-middleware" + "github.com/auth0/go-jwt-middleware/validate/josev2" + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" +) -## What is Auth0? +var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(jwtmiddleware.ContextKey{}) + j, err := json.MarshalIndent(user, "", "\t") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Println(err) + } -Auth0 helps you to: + fmt.Fprintf(w, "This is an authenticated request") + fmt.Fprintf(w, "Claim content:\n") + fmt.Fprint(w, string(j)) +}) -* Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**. -* Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**. -* Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user. -* Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely. -* Analytics of how, when and where users are logging in. -* Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules). +func main() { + keyFunc := func(ctx context.Context) (interface{}, error) { + // our token must be signed using this data + return []byte("secret"), nil + } -## Create a free Auth0 Account + expectedClaimsFunc := func() jwt.Expected { + // By setting up expected claims we are saying a token must + // have the data we specify. + return jwt.Expected{ + Issuer: "josev2-example", + } + } -1. Go to [Auth0](https://auth0.com) and click Sign Up. -2. Use Google, GitHub or Microsoft Account to login. + // setup the piece which will validate tokens + validator, err := josev2.New( + keyFunc, + jose.HS256, + josev2.WithExpectedClaims(expectedClaimsFunc), + ) + if err != nil { + // we'll panic in order to fail fast + panic(err) + } + + // setup the middleware + m := jwtmiddleware.New(validator.ValidateToken) + + http.ListenAndServe("0.0.0.0:3000", m.CheckJWT(handler)) +} +``` + +Running that code you can then curl it from another terminal: +``` +$ curl -H Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpc3MiOiJqb3NldjItZXhhbXBsZSJ9.e0lGglk9-m-n-t07eA5f7qgXGM-nD4ekwJkYVKprIUM" localhost:3000 +``` +should give you the response +``` +This is an authenticated requestClaim content: +{ + "CustomClaims": null, + "Claims": { + "iss": "josev2-example", + "sub": "1234567890", + "iat": 1516239022 + } +} +``` +The JWT included in the Authorization header above is signed with `secret`. + +To test it not working: +``` +$ curl -v -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.yiDw9IDNCa1WXCoDfPR_g356vSsHBEerqh9IvnD49QE" localhost:3000 +``` +should give you a response like +``` +... +< HTTP/1.1 401 Unauthorized +... +``` ## Issue Reporting @@ -27,8 +108,11 @@ If you have found a bug or if you have a feature request, please report them at ## Author -[Auth0](auth0.com) +[Auth0](https://auth0.com/) ## License This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. + +[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc Widget]: https://godoc.org/github.com/auth0/go-jwt-middleware?status.svg From e2b950ce4ad2aff49bfd1099238997e2834dd434 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:01:15 -0600 Subject: [PATCH 10/16] remove unneeded keys dir Signed-off-by: Jon Carl --- keys/sample-key | 1 - 1 file changed, 1 deletion(-) delete mode 100644 keys/sample-key diff --git a/keys/sample-key b/keys/sample-key deleted file mode 100644 index 47f557ef..00000000 --- a/keys/sample-key +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTQzMDc3OTMwNSwiZXhwIjoxNDYyMzE1MzA1LCJpYXQiOjE0MzA3NzkzMDUsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.KbVlagrOLiy-R65eUrVuno_IAjW-J5i_ySoSrs2SgjU From c77b2a15cc2992c861169ce4b50b4f94538704d8 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:02:50 -0600 Subject: [PATCH 11/16] correct copy/paste godoc error Signed-off-by: Jon Carl --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c592a0b..5cb4eae4 100644 --- a/README.md +++ b/README.md @@ -114,5 +114,5 @@ If you have found a bug or if you have a feature request, please report them at This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. -[GoDoc]: https://pkg.go.dev/github.com/go-chi/chi?tab=versions +[GoDoc]: https://pkg.go.dev/github.com/auth0/go-jwt-middleware?tab=versions [GoDoc Widget]: https://godoc.org/github.com/auth0/go-jwt-middleware?status.svg From 31e14a165fb0b057e669ea8a025b4e65e9628667 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:06:33 -0600 Subject: [PATCH 12/16] better go doc reference Signed-off-by: Jon Carl --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5cb4eae4..a82672c9 100644 --- a/README.md +++ b/README.md @@ -114,5 +114,5 @@ If you have found a bug or if you have a feature request, please report them at This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info. -[GoDoc]: https://pkg.go.dev/github.com/auth0/go-jwt-middleware?tab=versions -[GoDoc Widget]: https://godoc.org/github.com/auth0/go-jwt-middleware?status.svg +[GoDoc]: https://pkg.go.dev/github.com/auth0/go-jwt-middleware +[GoDoc Widget]: https://pkg.go.dev/badge/github.com/auth0/go-jwt-middleware.svg From 4563d183ef39f953d8d31f342f0de2cd8622e569 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:10:44 -0600 Subject: [PATCH 13/16] tidy up go modules Signed-off-by: Jon Carl --- go.mod | 10 +++------- go.sum | 49 ++++++++++++++++++++----------------------------- 2 files changed, 23 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index f6624903..da3329db 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,9 @@ module github.com/auth0/go-jwt-middleware go 1.14 require ( - github.com/form3tech-oss/jwt-go v3.2.2+incompatible - github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab + github.com/form3tech-oss/jwt-go v3.2.3+incompatible github.com/google/go-cmp v0.5.5 - github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect - github.com/gorilla/mux v1.7.4 - github.com/smartystreets/assertions v1.1.0 // indirect - github.com/smartystreets/goconvey v1.6.4 - github.com/urfave/negroni v1.0.0 + github.com/stretchr/testify v1.7.0 // indirect + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect gopkg.in/square/go-jose.v2 v2.5.1 ) diff --git a/go.sum b/go.sum index c13fca3e..73b53141 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,26 @@ -github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +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/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= +github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab h1:xveKWz2iaueeTaUgdetzel+U7exyigDYBryyVfV/rZk= -github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= -github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= -github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= -github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc= -github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -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/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 78203f3c457df3686e71c97ef8fac4b604fb5862 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:15:46 -0600 Subject: [PATCH 14/16] update http example Signed-off-by: Jon Carl --- examples/http-example/main.go | 62 +++++++++++++++++++++-------------- go.mod | 1 - go.sum | 2 -- 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/examples/http-example/main.go b/examples/http-example/main.go index 402f64b3..beed30f8 100644 --- a/examples/http-example/main.go +++ b/examples/http-example/main.go @@ -2,44 +2,56 @@ package main import ( "context" + "encoding/json" "fmt" "net/http" jwtmiddleware "github.com/auth0/go-jwt-middleware" - "github.com/form3tech-oss/jwt-go" + "github.com/auth0/go-jwt-middleware/validate/josev2" + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" ) -// TODO: replace this with default validate token func once it is merged in -func REPLACE_ValidateToken(_ context.Context, token string) (interface{}, error) { - // Now parse the token - parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { - return []byte("My Secret"), nil - }) - - // Check if there was an error in parsing... +var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user := r.Context().Value(jwtmiddleware.ContextKey{}) + j, err := json.MarshalIndent(user, "", "\t") if err != nil { - return nil, err - } - - // Check if the parsed token is valid... - if !parsedToken.Valid { - return nil, jwtmiddleware.ErrJWTInvalid + w.WriteHeader(http.StatusInternalServerError) + fmt.Println(err) } - return parsedToken, nil -} - -var myHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := r.Context().Value("user") fmt.Fprintf(w, "This is an authenticated request") fmt.Fprintf(w, "Claim content:\n") - for k, v := range user.(*jwt.Token).Claims.(jwt.MapClaims) { - fmt.Fprintf(w, "%s :\t%#v\n", k, v) - } + fmt.Fprint(w, string(j)) }) func main() { - jwtMiddleware := jwtmiddleware.New(REPLACE_ValidateToken) + keyFunc := func(ctx context.Context) (interface{}, error) { + // our token must be signed using this data + return []byte("secret"), nil + } + + expectedClaimsFunc := func() jwt.Expected { + // By setting up expected claims we are saying a token must + // have the data we specify. + return jwt.Expected{ + Issuer: "josev2-example", + } + } + + // setup the piece which will validate tokens + validator, err := josev2.New( + keyFunc, + jose.HS256, + josev2.WithExpectedClaims(expectedClaimsFunc), + ) + if err != nil { + // we'll panic in order to fail fast + panic(err) + } + + // setup the middleware + m := jwtmiddleware.New(validator.ValidateToken) - http.ListenAndServe("0.0.0.0:3000", jwtMiddleware.CheckJWT(myHandler)) + http.ListenAndServe("0.0.0.0:3000", m.CheckJWT(handler)) } diff --git a/go.mod b/go.mod index da3329db..aef6d8e9 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/auth0/go-jwt-middleware go 1.14 require ( - github.com/form3tech-oss/jwt-go v3.2.3+incompatible github.com/google/go-cmp v0.5.5 github.com/stretchr/testify v1.7.0 // indirect golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect diff --git a/go.sum b/go.sum index 73b53141..e229ff50 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ 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/form3tech-oss/jwt-go v3.2.3+incompatible h1:7ZaBxOI7TMoYBfyA3cQHErNNyAWIKUMIwqxEtgHOs5c= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 85a56f4b2ff8a1fd5f180de3fcd9f99cb3182b50 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:40:40 -0600 Subject: [PATCH 15/16] implement more classicist style testing Signed-off-by: Jon Carl --- jwtmiddleware_test.go | 88 ++++++++++++++++++++----------------------- 1 file changed, 41 insertions(+), 47 deletions(-) diff --git a/jwtmiddleware_test.go b/jwtmiddleware_test.go index fa57664b..492c2ea5 100644 --- a/jwtmiddleware_test.go +++ b/jwtmiddleware_test.go @@ -10,12 +10,32 @@ import ( "net/url" "testing" + "github.com/auth0/go-jwt-middleware/validate/josev2" "github.com/google/go-cmp/cmp" + "gopkg.in/square/go-jose.v2" + "gopkg.in/square/go-jose.v2/jwt" ) -// defaults tests against the default setup -// TODO(joncarl): replace with actual JWTs once we have the validate stuff plumbed in -func Test_defaults(t *testing.T) { +func Test(t *testing.T) { + var ( + validToken = "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0aW5nIn0.SdU_8KjnZsQChrVtQpYGxS48DxB4rTM9biq6D4haR70" + invalidToken = "bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ0ZXN0aW5nIn0.eM1Jd7VA7nFSI09FlmLmtuv7cLnv8qicZ8s76-jTOoE" + validContextToken = &josev2.UserContext{ + Claims: jwt.Claims{ + Issuer: "testing", + }, + } + ) + + validator, err := josev2.New( + func(_ context.Context) (interface{}, error) { return []byte("secret"), nil }, + jose.HS256, + josev2.WithExpectedClaims(func() jwt.Expected { return jwt.Expected{Issuer: "testing"} }), + ) + if err != nil { + t.Fatal(err) + } + tests := []struct { name string validateToken ValidateToken @@ -23,73 +43,55 @@ func Test_defaults(t *testing.T) { method string token string - wantToken map[string]string + wantToken interface{} wantStatusCode int wantBody string }{ { - name: "happy path", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return map[string]string{"foo": "bar"}, nil - }, - token: "bearer abc", - wantToken: map[string]string{"foo": "bar"}, + name: "happy path", + validateToken: validator.ValidateToken, + token: validToken, + wantToken: validContextToken, wantStatusCode: http.StatusOK, wantBody: "authenticated", }, { - name: "validate on options", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return map[string]string{"foo": "bar"}, nil - }, + name: "validate on options", + validateToken: validator.ValidateToken, method: http.MethodOptions, - token: "bearer abc", - wantToken: map[string]string{"foo": "bar"}, + token: validToken, + wantToken: validContextToken, wantStatusCode: http.StatusOK, wantBody: "authenticated", }, { - name: "bad token format", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return map[string]string{"foo": "bar"}, nil - }, - token: "abc", + name: "bad token format", + token: "bad", wantStatusCode: http.StatusInternalServerError, }, { - name: "credentials not optional", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return map[string]string{"foo": "bar"}, nil - }, + name: "credentials not optional", token: "", wantStatusCode: http.StatusBadRequest, }, { - name: "validate token errors", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("validate token error") - }, - token: "bearer abc", + name: "validate token errors", + validateToken: validator.ValidateToken, + token: invalidToken, wantStatusCode: http.StatusUnauthorized, }, { name: "validateOnOptions set to false", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since we are not validating on options") - }, options: []Option{ WithValidateOnOptions(false), }, method: http.MethodOptions, - token: "bearer abc", + token: validToken, wantStatusCode: http.StatusOK, wantBody: "authenticated", }, { name: "tokenExtractor errors", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since we are erroring on token extraction") - }, options: []Option{WithTokenExtractor(func(r *http.Request) (string, error) { return "", errors.New("token extractor error") })}, @@ -97,9 +99,6 @@ func Test_defaults(t *testing.T) { }, { name: "credentialsOptional true", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since credentials are optional and there are none") - }, options: []Option{ WithCredentialsOptional(true), WithTokenExtractor(func(r *http.Request) (string, error) { @@ -111,9 +110,6 @@ func Test_defaults(t *testing.T) { }, { name: "credentialsOptional false", - validateToken: func(_ context.Context, token string) (interface{}, error) { - return nil, errors.New("should not hit me since ErrJWTMissing should be returned") - }, options: []Option{ WithCredentialsOptional(false), WithTokenExtractor(func(r *http.Request) (string, error) { @@ -126,7 +122,7 @@ func Test_defaults(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var actualContextToken map[string]string + var actualContextToken interface{} if tc.method == "" { tc.method = http.MethodGet @@ -134,9 +130,7 @@ func Test_defaults(t *testing.T) { m := New(tc.validateToken, tc.options...) ts := httptest.NewServer(m.CheckJWT(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if ctxToken, ok := r.Context().Value(ContextKey{}).(map[string]string); ok { - actualContextToken = ctxToken - } + actualContextToken = r.Context().Value(ContextKey{}) fmt.Fprint(w, "authenticated") }))) defer ts.Close() From 67bbe077a2bb61c2125a53a30d8361c7fae8add2 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 14 May 2021 16:50:05 -0600 Subject: [PATCH 16/16] remove lint exclusion of SA1029 Signed-off-by: Jon Carl --- .github/workflows/lint.yaml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index eaef4fdb..fe4cc6ae 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -21,7 +21,7 @@ jobs: - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: - args: -v --timeout=5m --exclude SA1029 + args: -v --timeout=5m skip-build-cache: true skip-go-installation: true skip-pkg-cache: true diff --git a/Makefile b/Makefile index 2457b8c5..6e2e3585 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ test: ## Run tests. .PHONY: lint lint: ## Run golangci-lint. - golangci-lint run -v --timeout=5m --exclude SA1029 + golangci-lint run -v --timeout=5m .PHONY: help help: