Replace tests that depend on Apigee SSO API with tests that generate a JWT token themselves and verify against a public key stored in GitHub. Also, change the OAuth handler so that the stored public key is not a global variable.
diff --git a/oauth.go b/oauth.go index 5836d33..350c780 100644 --- a/oauth.go +++ b/oauth.go
@@ -4,23 +4,19 @@ "context" "crypto/rsa" "encoding/json" + "net/http" + "sync" + "time" + "github.com/SermoDigital/jose/crypto" "github.com/SermoDigital/jose/jws" "github.com/julienschmidt/httprouter" "github.com/justinas/alice" - "net/http" - "sync" - "time" -) - -var ( - gPkey *rsa.PublicKey = nil - rwMutex sync.RWMutex ) const params = "params" -/* Errors to return */ +// Errors to return type Errors []string /* @@ -36,14 +32,16 @@ } /* -OAuth structure that provides http connection to the URL that has the public +oauth provides http an connection to the URL that has the public key for verifying the JWT token */ -type OAuth struct { +type oauth struct { + gPkey *rsa.PublicKey + rwMutex *sync.RWMutex } /* -The interface functions offered to clients that act on OAuth param, +OAuthService offers interface functions that act on OAuth param, used to verify JWT tokens for the Http handler functions client wishes to validate against (via SSOHandler). */ @@ -58,18 +56,13 @@ verification. */ func (s *HTTPScaffold) CreateOAuth(keyURL string) OAuthService { - - pk, err := getPublicKey(keyURL) - if err == nil { - setPkSafe(pk) + pk, _ := getPublicKey(keyURL) + oa := &oauth{ + rwMutex: &sync.RWMutex{}, } - /* - * Routine that will fetch & update the public keys in safe manner - */ - updatePublicKeysPeriodic(keyURL) - - return &OAuth{} - + oa.setPkSafe(pk) + oa.updatePublicKeysPeriodic(keyURL) + return oa } /* @@ -92,7 +85,7 @@ SSOHandler offers the users the flexibility of choosing which http handlers need JWT validation. */ -func (a *OAuth) SSOHandler(p string, h func(http.ResponseWriter, *http.Request)) (string, httprouter.Handle) { +func (a *oauth) SSOHandler(p string, h func(http.ResponseWriter, *http.Request)) (string, httprouter.Handle) { return p, a.VerifyOAuth(alice.New().ThenFunc(h)) } @@ -100,7 +93,7 @@ VerifyOAuth verifies the JWT token in the request using the public key configured via CreateOAuth constructor. */ -func (a *OAuth) VerifyOAuth(next http.Handler) httprouter.Handle { +func (a *oauth) VerifyOAuth(next http.Handler) httprouter.Handle { return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { @@ -112,7 +105,7 @@ } /* Get the pulic key from cache */ - pk := getPkSafe() + pk := a.getPkSafe() if pk == nil { WriteErrorResponse(http.StatusBadRequest, "Public key not configured. Validation failed.", rw) return @@ -152,7 +145,7 @@ /* updatePulicKeysPeriodic updates the cache periodically (every hour) */ -func updatePublicKeysPeriodic(keyURL string) { +func (a *oauth) updatePublicKeysPeriodic(keyURL string) { ticker := time.NewTicker(time.Hour) quit := make(chan struct{}) @@ -162,7 +155,7 @@ case <-ticker.C: pk, err := getPublicKey(keyURL) if err == nil { - setPkSafe(pk) + a.setPkSafe(pk) } case <-quit: ticker.Stop() @@ -204,18 +197,18 @@ /* setPkSafe Safely stores the Public Key (via a Write Lock) */ -func setPkSafe(pk *rsa.PublicKey) { - rwMutex.Lock() - gPkey = pk - rwMutex.Unlock() +func (a *oauth) setPkSafe(pk *rsa.PublicKey) { + a.rwMutex.Lock() + a.gPkey = pk + a.rwMutex.Unlock() } /* getPkSafe returns the stored key (via a read lock) */ -func getPkSafe() *rsa.PublicKey { - rwMutex.RLock() - pk := gPkey - rwMutex.RUnlock() +func (a *oauth) getPkSafe() *rsa.PublicKey { + a.rwMutex.RLock() + pk := a.gPkey + a.rwMutex.RUnlock() return pk }
diff --git a/scaffold_test.go b/scaffold_test.go index 1b7a145..b2007b7 100644 --- a/scaffold_test.go +++ b/scaffold_test.go
@@ -6,23 +6,28 @@ "encoding/json" "errors" "fmt" - "github.com/julienschmidt/httprouter" "io/ioutil" "net" "net/http" - "os" "strings" "sync/atomic" "time" + "github.com/SermoDigital/jose/crypto" + "github.com/SermoDigital/jose/jws" + "github.com/julienschmidt/httprouter" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) +const ( + validJWTSigner = "https://raw.githubusercontent.com/30x/goscaffold/master/testkeys/jwtcert.json" + invalidJWTSigner = "https://raw.githubusercontent.com/30x/goscaffold/master/testkeys/notfound.json" +) + var ( - dbURL string - ssoURL string - bToken string + dbURL string ) var insecureClient = &http.Client{ @@ -32,21 +37,6 @@ }, }, } -var _ = BeforeSuite(func() { - ssoURL = os.Getenv("TEST_SSO_URL") - bToken = os.Getenv("BEARER_JWT_TOKEN") - if ssoURL == "" || bToken == "" { - fmt.Println("Tests aborted: TEST_SSO_URL/BEARER_JWT_TOKEN not set\n") - fmt.Println("Example:") - fmt.Println("TEST_SSO_URL=https://login.e2e.apigee.net/token_key") - fmt.Println("BEARER_JWT_TOKEN=eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwMDgwNWNlYi0yNzI5LTQ2OTgtYWNiMy1jNTRkZmIzMWM4MjEiLCJzdWIiO\n") - fmt.Println("NOTE:-") - fmt.Println("BEARER_JWT_TOKEN can be gotten by `get_token -u user@apigee.com:password`") - fmt.Println("get_token download, SSO_LOGIN_URL setup details are at https://apigeesc.atlassian.net/wiki/display/EH/get_token\n") - Fail("Please set Environment variables as expected") - } - -}) var _ = Describe("Scaffold Tests", func() { It("Validate framework", func() { @@ -396,45 +386,61 @@ Expect(scaf).ShouldNot(BeNil()) err := scaf.Open() Expect(err).Should(Succeed()) - oauth := scaf.CreateOAuth(ssoURL) + oauth := scaf.CreateOAuth(validJWTSigner) Expect(oauth).ShouldNot(BeNil()) go func() { fmt.Fprintf(GinkgoWriter, "Gonna listen on %s\n", scaf.InsecureAddress()) router.GET(oauth.SSOHandler("/foobar/:param1/:param2", buslogicHandler)) scaf.Listen(router) }() - Eventually(func() bool { - req, err := http.NewRequest("GET", + + Eventually(func() int { + req, reqerr := http.NewRequest("GET", "http://"+scaf.InsecureAddress()+"/foobar/xyz/123", nil) - if err != nil { - return false - } - req.Header.Set("Authorization", "Bearer "+bToken) + Expect(reqerr).Should(Succeed()) + req.Header.Set("Authorization", "Bearer "+string(createJWT())) client := &http.Client{} - resp, err := client.Do(req) - Expect(err).Should(Succeed()) + resp, reqerr := client.Do(req) + Expect(reqerr).Should(Succeed()) defer resp.Body.Close() - Expect(resp.StatusCode).To(Equal(http.StatusOK)) + return resp.StatusCode + }, 2*time.Second).Should(Equal(200)) - return true - }, 1*time.Second).Should(BeTrue()) + req, err := http.NewRequest("GET", + "http://"+scaf.InsecureAddress()+"/foobar/xyz/123", nil) + Expect(err).Should(Succeed()) + req.Header.Set("Authorization", "Bearer DEADBEEF") + client := &http.Client{} + resp, err := client.Do(req) + Expect(err).Should(Succeed()) + defer resp.Body.Close() + Expect(resp.StatusCode).Should(Equal(400)) + }) - Eventually(func() bool { - req, err := http.NewRequest("GET", - "http://"+scaf.InsecureAddress()+"/foobar/xyz/123", nil) - if err != nil { - return false - } - req.Header.Set("Authorization", "Bearer DEADBEEF") - client := &http.Client{} - resp, err := client.Do(req) - Expect(err).Should(Succeed()) - defer resp.Body.Close() - Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) + It("SSO handler validation bad public key", func() { + router := httprouter.New() + Expect(router).ShouldNot(BeNil()) + scaf := CreateHTTPScaffold() + Expect(scaf).ShouldNot(BeNil()) + err := scaf.Open() + Expect(err).Should(Succeed()) + oauth := scaf.CreateOAuth(invalidJWTSigner) + Expect(oauth).ShouldNot(BeNil()) + go func() { + fmt.Fprintf(GinkgoWriter, "Gonna listen on %s\n", scaf.InsecureAddress()) + router.GET(oauth.SSOHandler("/foobar/:param1/:param2", buslogicHandler)) + scaf.Listen(router) + }() - return true - }, 1*time.Second).Should(BeTrue()) - + req, err := http.NewRequest("GET", + "http://"+scaf.InsecureAddress()+"/foobar/xyz/123", nil) + Expect(err).Should(Succeed()) + req.Header.Set("Authorization", "Bearer "+string(createJWT())) + client := &http.Client{} + resp, err := client.Do(req) + Expect(err).Should(Succeed()) + defer resp.Body.Close() + Expect(resp.StatusCode).Should(Equal(400)) }) It("Get stack trace", func() { @@ -443,6 +449,22 @@ Expect(b.Len()).ShouldNot(BeZero()) }) + It("Verify JWT creation", func() { + // Ensure that our logic in this test for creating a JWT really works + jwt := createJWT() + fmt.Fprintf(GinkgoWriter, "JWT: %s\n", string(jwt)) + + certBytes, err := ioutil.ReadFile("./testkeys/jwtcert.pem") + Expect(err).Should(Succeed()) + cert, err := crypto.ParseRSAPublicKeyFromPEM(certBytes) + Expect(err).Should(Succeed()) + + parsedJwt, err := jws.ParseJWT(jwt) + Expect(err).Should(Succeed()) + + err = parsedJwt.Validate(cert, crypto.SigningMethodRS256) + Expect(err).Should(Succeed()) + }) }) func buslogicHandler(w http.ResponseWriter, r *http.Request) { @@ -566,3 +588,24 @@ } return "" } + +func createJWT() []byte { + keyBytes, err := ioutil.ReadFile("./testkeys/jwtkey.pem") + Expect(err).Should(Succeed()) + pk, err := crypto.ParseRSAPrivateKeyFromPEM(keyBytes) + Expect(err).Should(Succeed()) + + claims := jws.Claims{} + now := time.Now() + claims.SetAudience("http://github.com/30x/goscaffold") + claims.SetIssuer("http://github.com/30x/goscaffold") + claims.SetSubject("http://github.com/30x/goscaffold") + claims.SetIssuedAt(now) + claims.SetNotBefore(now) + claims.SetExpiration(now.Add(time.Hour)) + jwt := jws.NewJWT(claims, crypto.SigningMethodRS256) + + rawJwt, err := jwt.Serialize(pk) + Expect(err).Should(Succeed()) + return rawJwt +}