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
+}