From 0d99313c4ac3fedbaec3a48486d4c93a7d2fd698 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 25 Jun 2021 17:47:34 -0600 Subject: [PATCH 01/11] add JWKSProvider for josev2 Signed-off-by: Jon Carl --- internal/oidc/oidc.go | 39 +++++ validate/josev2/josev2.go | 119 +++++++++++++- validate/josev2/josev2_test.go | 275 ++++++++++++++++++++++++++++++++- 3 files changed, 430 insertions(+), 3 deletions(-) create mode 100644 internal/oidc/oidc.go diff --git a/internal/oidc/oidc.go b/internal/oidc/oidc.go new file mode 100644 index 0000000..02a6589 --- /dev/null +++ b/internal/oidc/oidc.go @@ -0,0 +1,39 @@ +package oidc + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "path" +) + +// WellKnownEndpoints holds the well known OIDC endpoints +type WellKnownEndpoints struct { + JWKSURI string `json:"jwks_uri"` +} + +// GetWellKnownEndpointsFromIssuerURL gets the well known endpoints for the +// passed in issuer url +func GetWellKnownEndpointsFromIssuerURL(ctx context.Context, issuerURL url.URL) (*WellKnownEndpoints, error) { + issuerURL.Path = path.Join(issuerURL.Path, ".well-known/openid-configuration") + + req, err := http.NewRequest(http.MethodGet, issuerURL.String(), nil) + if err != nil { + return nil, fmt.Errorf("could not build request to get well known endpoints: %w", err) + } + req = req.WithContext(ctx) + + r, err := http.DefaultClient.Do(req) + if err != nil { + return nil, fmt.Errorf("could not get well known endpoints from url %s: %w", issuerURL.String(), err) + } + var wkEndpoints WellKnownEndpoints + err = json.NewDecoder(r.Body).Decode(&wkEndpoints) + if err != nil { + return nil, fmt.Errorf("could not decode json body when getting well known endpoints: %w", err) + } + + return &wkEndpoints, nil +} diff --git a/validate/josev2/josev2.go b/validate/josev2/josev2.go index 4e76609..9a3f6e9 100644 --- a/validate/josev2/josev2.go +++ b/validate/josev2/josev2.go @@ -2,10 +2,15 @@ package josev2 import ( "context" + "encoding/json" "errors" "fmt" + "net/http" + "net/url" + "sync" "time" + "github.com/auth0/go-jwt-middleware/internal/oidc" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -115,7 +120,7 @@ func (v *Validator) ValidateToken(ctx context.Context, token string) (interface{ // 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) + return nil, fmt.Errorf("expected %q signing algorithm but token specified %q", signatureAlgorithm, tok.Headers[0].Algorithm) } key, err := v.keyFunc(ctx) @@ -133,7 +138,8 @@ func (v *Validator) ValidateToken(ctx context.Context, token string) (interface{ } userCtx := &UserContext{ - Claims: *claimDest[0].(*jwt.Claims), + CustomClaims: nil, + Claims: *claimDest[0].(*jwt.Claims), } if err = userCtx.Claims.ValidateWithLeeway(v.expectedClaims(), v.allowedClockSkew); err != nil { @@ -149,3 +155,112 @@ func (v *Validator) ValidateToken(ctx context.Context, token string) (interface{ return userCtx, nil } + +// JWKSProvider handles getting JWKS from the specified IssuerURL and exposes +// KeyFunc which adheres to the keyFunc signature that the Validator requires. +// Most likely you will want to use the CachingJWKSProvider as it handles +// getting and caching JWKS which can help reduce request time and potential +// rate limiting from your provider. +type JWKSProvider struct { + IssuerURL url.URL +} + +// NewJWKSProvider builds and returns a new JWKSProvider. +func NewJWKSProvider(issuerURL url.URL) *JWKSProvider { + return &JWKSProvider{IssuerURL: issuerURL} +} + +// KeyFunc adheres to the keyFunc signature that the Validator requires. While +// it returns an interface to adhere to keyFunc, as long as the error is nil +// the type will be *jose.JSONWebKeySet. +func (p *JWKSProvider) KeyFunc(ctx context.Context) (interface{}, error) { + wkEndpoints, err := oidc.GetWellKnownEndpointsFromIssuerURL(ctx, p.IssuerURL) + if err != nil { + return nil, err + } + + u, err := url.Parse(wkEndpoints.JWKSURI) + if err != nil { + return nil, fmt.Errorf("could not parse JWKS URI from well known endpoints: %w", err) + } + + req, err := http.NewRequest(http.MethodGet, u.String(), nil) + if err != nil { + return nil, fmt.Errorf("could not build request to get JWKS: %w", err) + } + req = req.WithContext(ctx) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var jwks jose.JSONWebKeySet + if err := json.NewDecoder(resp.Body).Decode(&jwks); err != nil { + return nil, fmt.Errorf("could not decode jwks: %w", err) + } + + return &jwks, nil +} + +type cachedJWKS struct { + jwks *jose.JSONWebKeySet + expiresAt time.Time +} + +// CachingJWKSProvider handles getting JWKS from the specified IssuerURL and +// caching them for CacheTTL time. It exposes KeyFunc which adheres to the +// keyFunc signature that the Validator requires. +type CachingJWKSProvider struct { + IssuerURL url.URL + CacheTTL time.Duration + + mu sync.Mutex + cache map[string]cachedJWKS +} + +// NewCachingJWKSProvider builds and returns a new CachingJWKSProvider. If +// cacheTTL is zero then a default value of 1 minute will be used. +func NewCachingJWKSProvider(issuerURL url.URL, cacheTTL time.Duration) *CachingJWKSProvider { + if cacheTTL == 0 { + cacheTTL = 1 * time.Minute + } + + return &CachingJWKSProvider{ + IssuerURL: issuerURL, + CacheTTL: cacheTTL, + cache: map[string]cachedJWKS{}, + } +} + +// KeyFunc adheres to the keyFunc signature that the Validator requires. While +// it returns an interface to adhere to keyFunc, as long as the error is nil +// the type will be *jose.JSONWebKeySet. +func (c *CachingJWKSProvider) KeyFunc(ctx context.Context) (interface{}, error) { + issuer := c.IssuerURL.Hostname() + + c.mu.Lock() + defer func() { + c.mu.Unlock() + }() + + if cached, ok := c.cache[issuer]; ok { + if !time.Now().After(cached.expiresAt) { + return cached.jwks, nil + } + } + + p := JWKSProvider{IssuerURL: c.IssuerURL} + jwks, err := p.KeyFunc(ctx) + if err != nil { + return nil, err + } + + c.cache[issuer] = cachedJWKS{ + jwks: jwks.(*jose.JSONWebKeySet), + expiresAt: time.Now().Add(c.CacheTTL), + } + + return jwks, nil +} diff --git a/validate/josev2/josev2_test.go b/validate/josev2/josev2_test.go index 864a2d3..4c3f81b 100644 --- a/validate/josev2/josev2_test.go +++ b/validate/josev2/josev2_test.go @@ -2,9 +2,20 @@ package josev2 import ( "context" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/json" "errors" + "math/big" + "net/http" + "net/http/httptest" + "net/url" + "sync" "testing" + "time" + "github.com/auth0/go-jwt-middleware/internal/oidc" "github.com/google/go-cmp/cmp" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" @@ -51,7 +62,7 @@ func Test_Validate(t *testing.T) { name: "errors on wrong algorithm", signatureAlgorithm: jose.PS256, token: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o`, - expectedError: "expected \"PS256\" signin algorithm but token specified \"HS256\"", + expectedError: "expected \"PS256\" signing algorithm but token specified \"HS256\"", }, { name: "errors when jwt.ParseSigned errors", @@ -154,3 +165,265 @@ func Test_New(t *testing.T) { }) } + +func Test_JWKSProvider(t *testing.T) { + var ( + p CachingJWKSProvider + server *httptest.Server + responseBytes []byte + responseStatusCode, reqCount int + serverURL *url.URL + ) + + tests := []struct { + name string + main func(t *testing.T) + }{ + { + name: "calls out to well known endpoint", + main: func(t *testing.T) { + _, jwks := genValidRSAKeyAndJWKS(t) + var err error + responseBytes, err = json.Marshal(jwks) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + _, err = p.KeyFunc(context.TODO()) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + }, + }, + { + name: "errors if it can't decode the jwks", + main: func(t *testing.T) { + responseBytes = []byte("<>") + _, err := p.KeyFunc(context.TODO()) + + wantErr := "could not decode jwks: invalid character '<' looking for beginning of value" + if !equalErrors(err, wantErr) { + t.Fatalf("wanted err:\n%s\ngot:\n%+v\n", wantErr, err) + } + }, + }, + { + name: "passes back the valid jwks", + main: func(t *testing.T) { + _, jwks := genValidRSAKeyAndJWKS(t) + var err error + responseBytes, err = json.Marshal(jwks) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + p.CacheTTL = time.Minute * 5 + actualJWKS, err := p.KeyFunc(context.TODO()) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + if want, got := &jwks, actualJWKS; !cmp.Equal(want, got) { + t.Fatalf("jwks did not match: %s", cmp.Diff(want, got)) + } + + if want, got := &jwks, p.cache[serverURL.Hostname()].jwks; !cmp.Equal(want, got) { + t.Fatalf("cached jwks did not match: %s", cmp.Diff(want, got)) + } + + expiresAt := p.cache[serverURL.Hostname()].expiresAt + if !time.Now().Before(expiresAt) { + t.Fatalf("wanted cache item expiration to be in the future but it was not: %s", expiresAt) + } + }, + }, + { + name: "returns the cached jwks when they are not expired", + main: func(t *testing.T) { + _, expectedCachedJWKS := genValidRSAKeyAndJWKS(t) + p.cache[serverURL.Hostname()] = cachedJWKS{ + jwks: &expectedCachedJWKS, + expiresAt: time.Now().Add(1 * time.Minute), + } + + actualJWKS, err := p.KeyFunc(context.TODO()) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + if want, got := &expectedCachedJWKS, actualJWKS; !cmp.Equal(want, got) { + t.Fatalf("cached jwks did not match: %s", cmp.Diff(want, got)) + } + + if reqCount > 0 { + t.Fatalf("did not want any requests since we should have read from the cache, but we got %d requests", reqCount) + } + }, + }, + { + name: "re-caches the jwks if they have expired", + main: func(t *testing.T) { + _, expiredCachedJWKS := genValidRSAKeyAndJWKS(t) + expiresAt := time.Now().Add(-10 * time.Minute) + p.cache[server.URL] = cachedJWKS{ + jwks: &expiredCachedJWKS, + expiresAt: expiresAt, + } + _, jwks := genValidRSAKeyAndJWKS(t) + var err error + responseBytes, err = json.Marshal(jwks) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + p.CacheTTL = time.Minute * 5 + actualJWKS, err := p.KeyFunc(context.TODO()) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + if want, got := &jwks, actualJWKS; !cmp.Equal(want, got) { + t.Fatalf("jwks did not match: %s", cmp.Diff(want, got)) + } + + if want, got := &jwks, p.cache[serverURL.Hostname()].jwks; !cmp.Equal(want, got) { + t.Fatalf("cached jwks did not match: %s", cmp.Diff(want, got)) + } + + cacheExpiresAt := p.cache[serverURL.Hostname()].expiresAt + if !time.Now().Before(cacheExpiresAt) { + t.Fatalf("wanted cache item expiration to be in the future but it was not: %s", cacheExpiresAt) + } + }, + }, + { + name: "only calls the API once when multiple requests come in", + main: func(t *testing.T) { + _, jwks := genValidRSAKeyAndJWKS(t) + var err error + responseBytes, err = json.Marshal(jwks) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + p.CacheTTL = time.Minute * 5 + + wg := sync.WaitGroup{} + for i := 0; i < 50; i++ { + wg.Add(1) + go func(t *testing.T) { + actualJWKS, err := p.KeyFunc(context.TODO()) + if !equalErrors(err, "") { + t.Errorf("did not want an error, but got %s", err) + } + + if want, got := &jwks, actualJWKS; !cmp.Equal(want, got) { + t.Errorf("jwks did not match: %s", cmp.Diff(want, got)) + } + + wg.Done() + }(t) + } + wg.Wait() + + actualJWKS, err := p.KeyFunc(context.TODO()) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + if want, got := &jwks, actualJWKS; !cmp.Equal(want, got) { + t.Fatalf("jwks did not match: %s", cmp.Diff(want, got)) + } + + if reqCount != 2 { + t.Fatalf("only wanted 2 requests (well known and jwks) , but we got %d requests", reqCount) + } + + if want, got := &jwks, p.cache[serverURL.Hostname()].jwks; !cmp.Equal(want, got) { + t.Fatalf("cached jwks did not match: %s", cmp.Diff(want, got)) + } + + cacheExpiresAt := p.cache[serverURL.Hostname()].expiresAt + if !time.Now().Before(cacheExpiresAt) { + t.Fatalf("wanted cache item expiration to be in the future but it was not: %s", cacheExpiresAt) + } + }, + }, + } + + for _, test := range tests { + var reqCallMutex sync.Mutex + + reqCount = 0 + responseBytes = []byte(`{"kid":""}`) + responseStatusCode = http.StatusOK + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // handle mutex things + reqCallMutex.Lock() + defer reqCallMutex.Unlock() + reqCount++ + w.WriteHeader(responseStatusCode) + + switch r.URL.String() { + case "/.well-known/openid-configuration": + wk := oidc.WellKnownEndpoints{JWKSURI: server.URL + "/url_for_jwks"} + json.NewEncoder(w).Encode(wk) + case "/url_for_jwks": + _, err := w.Write(responseBytes) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + default: + t.Fatalf("do not know how to handle url %s", r.URL.String()) + } + + })) + defer server.Close() + serverURL = mustParseURL(server.URL) + + p = CachingJWKSProvider{ + IssuerURL: *serverURL, + CacheTTL: 0, + cache: map[string]cachedJWKS{}, + } + + t.Run(test.name, test.main) + } +} + +func mustParseURL(toParse string) *url.URL { + parsed, err := url.Parse(toParse) + if err != nil { + panic(err) + } + + return parsed +} + +func genValidRSAKeyAndJWKS(t *testing.T) (*rsa.PrivateKey, jose.JSONWebKeySet) { + ca := &x509.Certificate{ + SerialNumber: big.NewInt(1653), + } + priv, _ := rsa.GenerateKey(rand.Reader, 2048) + rawCert, err := x509.CreateCertificate(rand.Reader, ca, ca, &priv.PublicKey, priv) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + jwks := jose.JSONWebKeySet{ + Keys: []jose.JSONWebKey{ + { + Key: priv, + KeyID: "kid", + Certificates: []*x509.Certificate{ + { + Raw: rawCert, + }, + }, + CertificateThumbprintSHA1: []uint8{}, + CertificateThumbprintSHA256: []uint8{}, + }, + }, + } + return priv, jwks +} From e60df549f291303c84011c99943250bdc6698a67 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 14:08:49 -0600 Subject: [PATCH 02/11] add info in example on using the caching key provider Signed-off-by: Jon Carl --- examples/http-example/main.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/examples/http-example/main.go b/examples/http-example/main.go index beed30f..bc4ba5a 100644 --- a/examples/http-example/main.go +++ b/examples/http-example/main.go @@ -26,6 +26,15 @@ var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { }) func main() { + // uncomment the below to use the caching key provider + // u, err := url.Parse("https://") + // if err != nil { + // // we'll panic in order to fail fast + // panic(err) + // } + + // p := josev2.NewCachingJWKSProvider(*u, 5*time.Minute) + keyFunc := func(ctx context.Context) (interface{}, error) { // our token must be signed using this data return []byte("secret"), nil @@ -41,6 +50,7 @@ func main() { // setup the piece which will validate tokens validator, err := josev2.New( + // p.KeyFunc, // uncomment this to use the caching key provider keyFunc, jose.HS256, josev2.WithExpectedClaims(expectedClaimsFunc), From 58a0c0f1417a4ecd8736a91294ade9277c83b50c Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 14:25:21 -0600 Subject: [PATCH 03/11] fix lint issue Signed-off-by: Jon Carl --- validate/josev2/josev2_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/validate/josev2/josev2_test.go b/validate/josev2/josev2_test.go index 4c3f81b..185d6e3 100644 --- a/validate/josev2/josev2_test.go +++ b/validate/josev2/josev2_test.go @@ -367,7 +367,10 @@ func Test_JWKSProvider(t *testing.T) { switch r.URL.String() { case "/.well-known/openid-configuration": wk := oidc.WellKnownEndpoints{JWKSURI: server.URL + "/url_for_jwks"} - json.NewEncoder(w).Encode(wk) + err := json.NewEncoder(w).Encode(wk) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } case "/url_for_jwks": _, err := w.Write(responseBytes) if !equalErrors(err, "") { From 2cfedd27ac3d3f46d5c972f8615161ef8eccdae6 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 14:29:59 -0600 Subject: [PATCH 04/11] fix cmp issue Signed-off-by: Jon Carl --- validate/josev2/josev2_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/validate/josev2/josev2_test.go b/validate/josev2/josev2_test.go index 185d6e3..a2d7d4b 100644 --- a/validate/josev2/josev2_test.go +++ b/validate/josev2/josev2_test.go @@ -17,6 +17,7 @@ import ( "github.com/auth0/go-jwt-middleware/internal/oidc" "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -223,8 +224,8 @@ func Test_JWKSProvider(t *testing.T) { t.Fatalf("did not want an error, but got %s", err) } - if want, got := &jwks, actualJWKS; !cmp.Equal(want, got) { - t.Fatalf("jwks did not match: %s", cmp.Diff(want, got)) + if want, got := &jwks, actualJWKS; !cmp.Equal(want, got, cmpopts.IgnoreUnexported()) { + t.Fatalf("jwks did not match: %s", cmp.Diff(want, got, cmpopts.IgnoreUnexported())) } if want, got := &jwks, p.cache[serverURL.Hostname()].jwks; !cmp.Equal(want, got) { From 8e985cf2451b796e60188089331760dc10529c0e Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 14:33:44 -0600 Subject: [PATCH 05/11] try a new version of cmp Signed-off-by: Jon Carl --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index dae5029..c3eacc0 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.14 require ( github.com/golang-jwt/jwt v3.2.1+incompatible - github.com/google/go-cmp v0.5.5 + github.com/google/go-cmp v0.5.6 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 2fcb062..f860fbd 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfE github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= 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/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/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= From fa979f32b603c1f9c4d76568527da8ac8cdc3806 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 14:52:36 -0600 Subject: [PATCH 06/11] try a different strategy for cmp Signed-off-by: Jon Carl --- validate/josev2/josev2_test.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/validate/josev2/josev2_test.go b/validate/josev2/josev2_test.go index a2d7d4b..54e318c 100644 --- a/validate/josev2/josev2_test.go +++ b/validate/josev2/josev2_test.go @@ -17,7 +17,6 @@ import ( "github.com/auth0/go-jwt-middleware/internal/oidc" "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" "gopkg.in/square/go-jose.v2" "gopkg.in/square/go-jose.v2/jwt" ) @@ -224,8 +223,18 @@ func Test_JWKSProvider(t *testing.T) { t.Fatalf("did not want an error, but got %s", err) } - if want, got := &jwks, actualJWKS; !cmp.Equal(want, got, cmpopts.IgnoreUnexported()) { - t.Fatalf("jwks did not match: %s", cmp.Diff(want, got, cmpopts.IgnoreUnexported())) + jwksJSON, err := json.Marshal(jwks) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + actualJWKSJSON, err := json.Marshal(actualJWKS) + if !equalErrors(err, "") { + t.Fatalf("did not want an error, but got %s", err) + } + + if want, got := jwksJSON, actualJWKSJSON; !cmp.Equal(want, got) { + t.Fatalf("jwks did not match: %s", cmp.Diff(want, got)) } if want, got := &jwks, p.cache[serverURL.Hostname()].jwks; !cmp.Equal(want, got) { From 8a5baf85e72f03123f1d0206238ed21dad5f2918 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 15:07:36 -0600 Subject: [PATCH 07/11] try a different version of ubuntu Signed-off-by: Jon Carl --- .github/workflows/test.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index e57dd21..a52b612 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,7 +11,7 @@ jobs: test: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: [ubuntu-18.04, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: install go From 424235d25b3c7d1a4d0be2c219e98ee70513442b Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 15:29:12 -0600 Subject: [PATCH 08/11] try a different version of golang Signed-off-by: Jon Carl --- .github/workflows/test.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a52b612..45e2612 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -11,13 +11,13 @@ jobs: test: strategy: matrix: - os: [ubuntu-18.04, macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - name: install go uses: actions/setup-go@v1 with: - go-version: 1.14 + go-version: 1.16 - name: checkout code uses: actions/checkout@v2 - name: test From b878134e3859b7766c8fc6310cf8b8a19416ad35 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 9 Jul 2021 15:45:10 -0600 Subject: [PATCH 09/11] revert change in tests Signed-off-by: Jon Carl --- validate/josev2/josev2_test.go | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/validate/josev2/josev2_test.go b/validate/josev2/josev2_test.go index 54e318c..185d6e3 100644 --- a/validate/josev2/josev2_test.go +++ b/validate/josev2/josev2_test.go @@ -223,17 +223,7 @@ func Test_JWKSProvider(t *testing.T) { t.Fatalf("did not want an error, but got %s", err) } - jwksJSON, err := json.Marshal(jwks) - if !equalErrors(err, "") { - t.Fatalf("did not want an error, but got %s", err) - } - - actualJWKSJSON, err := json.Marshal(actualJWKS) - if !equalErrors(err, "") { - t.Fatalf("did not want an error, but got %s", err) - } - - if want, got := jwksJSON, actualJWKSJSON; !cmp.Equal(want, got) { + if want, got := &jwks, actualJWKS; !cmp.Equal(want, got) { t.Fatalf("jwks did not match: %s", cmp.Diff(want, got)) } From 28503589c90a9e6b8f6bd8413c04f3bb20359fb7 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 16 Jul 2021 13:22:52 -0600 Subject: [PATCH 10/11] add jwks example Signed-off-by: Jon Carl --- examples/http-example/main.go | 12 +------ examples/http-jwks-example/README.md | 15 ++++++++ examples/http-jwks-example/main.go | 51 ++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 examples/http-jwks-example/README.md create mode 100644 examples/http-jwks-example/main.go diff --git a/examples/http-example/main.go b/examples/http-example/main.go index bc4ba5a..c21c9e2 100644 --- a/examples/http-example/main.go +++ b/examples/http-example/main.go @@ -20,21 +20,12 @@ var handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Println(err) } - fmt.Fprintf(w, "This is an authenticated request") + fmt.Fprintf(w, "This is an authenticated request\n") fmt.Fprintf(w, "Claim content:\n") fmt.Fprint(w, string(j)) }) func main() { - // uncomment the below to use the caching key provider - // u, err := url.Parse("https://") - // if err != nil { - // // we'll panic in order to fail fast - // panic(err) - // } - - // p := josev2.NewCachingJWKSProvider(*u, 5*time.Minute) - keyFunc := func(ctx context.Context) (interface{}, error) { // our token must be signed using this data return []byte("secret"), nil @@ -50,7 +41,6 @@ func main() { // setup the piece which will validate tokens validator, err := josev2.New( - // p.KeyFunc, // uncomment this to use the caching key provider keyFunc, jose.HS256, josev2.WithExpectedClaims(expectedClaimsFunc), diff --git a/examples/http-jwks-example/README.md b/examples/http-jwks-example/README.md new file mode 100644 index 0000000..c0448b5 --- /dev/null +++ b/examples/http-jwks-example/README.md @@ -0,0 +1,15 @@ +# HTTP JWKS example + +This is an example of how to use the http middleware with JWKS. + +# Using it + +To try this out: +1. Install all dependencies with `go install` +1. Go to https://manage.auth0.com/ and create a new API. +1. Go to the "Test" tab of the API and copy the cURL example. +1. Run the cURL example in your terminal and copy the `access_token` from the response. The tool jq can be helpful for this. +1. In the example change `` on line 29 to the domain used in the cURL request. +1. Run the example with `go run main.go`. +1. In a new terminal use cURL to talk to the API: `curl -v --request GET --url http://localhost:3000` +1. Now try it again with the `access_token` you copied earlier and run `curl -v --request GET --url http://localhost:3000 --header "authorization: Bearer $TOKEN"` to see a successful request. diff --git a/examples/http-jwks-example/main.go b/examples/http-jwks-example/main.go new file mode 100644 index 0000000..0e19a9d --- /dev/null +++ b/examples/http-jwks-example/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "time" + + jwtmiddleware "github.com/auth0/go-jwt-middleware" + "github.com/auth0/go-jwt-middleware/validate/josev2" + "gopkg.in/square/go-jose.v2" +) + +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) + } + + fmt.Fprintf(w, "This is an authenticated request\n") + fmt.Fprintf(w, "Claim content:\n") + fmt.Fprint(w, string(j)) +}) + +func main() { + u, err := url.Parse("https://") + if err != nil { + // we'll panic in order to fail fast + panic(err) + } + + p := josev2.NewCachingJWKSProvider(*u, 5*time.Minute) + + // setup the piece which will validate tokens + validator, err := josev2.New( + p.KeyFunc, + jose.RS256, + ) + 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)) +} From c3d388acde047a170c12fdc977399eba5a4665d0 Mon Sep 17 00:00:00 2001 From: Jon Carl Date: Fri, 16 Jul 2021 13:29:35 -0600 Subject: [PATCH 11/11] add jwks example reference to the josev2 examples Signed-off-by: Jon Carl --- validate/josev2/examples/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/validate/josev2/examples/README.md b/validate/josev2/examples/README.md index 6bcee7d..d8ba97b 100644 --- a/validate/josev2/examples/README.md +++ b/validate/josev2/examples/README.md @@ -81,5 +81,7 @@ 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) ``` +### JWKS +For a JWKS example please see [examples/http-jwks-example/README.md](../../../examples/http-jwks-example/README.md). Take a look through the example code and things will make a lot more sense.