diff --git a/README.md b/README.md index 6fc6479..b84ba77 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ This is the client SDK for Keploy API testing platform. There are 2 modes: 4. [Supported Routers](#supported-routers) 5. [Supported Databases](#supported-databases) 6. [Support Clients](#supported-clients) +7. [Supported JWT Middlewares](#supported-jwt-middlewares) ## Installation ```bash @@ -415,3 +416,127 @@ k := keploy.New(keploy.Config{ conn, err := grpc.Dial(address, grpc.WithInsecure(), kgrpc.WithClientUnaryInterceptor(k)) ``` **Note**: Currently streaming is not yet supported. + +## Supported JWT Middlewares +### jwtauth +Middlewares which can be used to authenticate. It is compatible for Chi, Gin and Echo router. Usage is similar to go-chi/jwtauth. Adds ValidationOption to mock time in test mode. + +#### Example +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/go-chi/chi" + "github.com/labstack/echo/v4" + + "github.com/benbjohnson/clock" + "github.com/keploy/go-sdk/integrations/kchi" + "github.com/keploy/go-sdk/integrations/kecho/v4" + "github.com/keploy/go-sdk/integrations/kgin/v1" + + "github.com/keploy/go-sdk/integrations/kjwtauth" + "github.com/keploy/go-sdk/keploy" +) + +var ( + kApp *keploy.Keploy + tokenAuth *kjwtauth.JWTAuth +) + +func init() { + // Initialize kaploy instance + port := "6060" + kApp = keploy.New(keploy.Config{ + App: keploy.AppConfig{ + Name: "client-echo-App", + Port: port, + }, + Server: keploy.ServerConfig{ + URL: "http://localhost:8081/api", + }, + }) + // Generate a JWTConfig + tokenAuth = kjwtauth.New("HS256", []byte("mysecret"), nil, kApp) + + claims := map[string]interface{}{"user_id": 123} + kjwtauth.SetExpiryIn(claims, 20*time.Second) + // Create a token string + _, tokenString, _ := tokenAuth.Encode(claims) + fmt.Printf("DEBUG: a sample jwt is %s\n\n", tokenString) +} + +func main() { + addr := ":6060" + + fmt.Printf("Starting server on %v\n", addr) + http.ListenAndServe(addr, echoRouter()) +} + +func chiRouter() http.Handler { + // Chi example(comment echo, gin to use chi) + r := chi.NewRouter() + kchi.ChiV5(kApp, r) + // Protected routes + r.Group(func(r chi.Router) { + // Seek, verify and validate JWT tokens + r.Use(kjwtauth.VerifierChi(tokenAuth)) + + // Handle valid / invalid tokens. In this example, we use + // the provided authenticator middleware, but you can write your + // own very easily, look at the Authenticator method in jwtauth.go + // and tweak it, its not scary. + r.Use(kjwtauth.AuthenticatorChi) + + r.Get("/admin", func(w http.ResponseWriter, r *http.Request) { + _, claims, _ := kjwtauth.FromContext(r.Context()) + fmt.Println("requested admin") + w.Write([]byte(fmt.Sprintf("protected area, Hi %v", claims["user_id"]))) + }) + }) + // Public routes + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("welcome")) + }) + + return r +} + +func echoRouter() http.Handler { + // Echo example + er := echo.New() + // add keploy's echo middleware + kecho.EchoV4(kApp, er) + // Public route + er.GET("/", func(c echo.Context) error { + return c.String(http.StatusOK, "Accessible") + }) + // Protected route + er.GET("echoAdmin", func(c echo.Context) error { + _, claims, _ := kjwtauth.FromContext(c.Request().Context()) + fmt.Println("requested admin") + return c.String(http.StatusOK, fmt.Sprint("protected area, Hi fin user: %v", claims["user_id"])) + }, kjwtauth.VerifierEcho(tokenAuth), kjwtauth.AuthenticatorEcho) + return er +} + +func ginRouter() http.Handler { + // Gin example(comment echo example to use gin) + gr := gin.New() + kgin.GinV1(kApp, gr) + // Public route + gr.GET("/", func(ctx *gin.Context) { + ctx.Writer.Write([]byte("welcome to gin")) + }) + // Protected route + auth := gr.Group("/auth") + auth.Use(kjwtauth.VerifierGin(tokenAuth)) + auth.Use(kjwtauth.AuthenticatorGin) + auth.GET("/ginAdmin", func(c *gin.Context) { + _, claims, _ := kjwtauth.FromContext(c.Request.Context()) + fmt.Println("requested admin") + c.Writer.Write([]byte(fmt.Sprintf("protected area, Hi fin user: %v", claims["user_id"]))) + }) + return gr +} +``` diff --git a/go.mod b/go.mod index 03a5190..b05ee22 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/keploy/go-sdk go 1.17 +// replace "go.keploy.io/server" => ../keploy require ( github.com/aws/aws-sdk-go v1.42.23 github.com/bnkamalesh/webgo/v4 v4.1.11 @@ -16,10 +17,21 @@ require ( ) require ( + github.com/benbjohnson/clock v1.1.0 github.com/go-chi/chi v1.5.4 go.keploy.io/server v0.1.8 ) +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d // indirect + github.com/goccy/go-json v0.9.4 // indirect + github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect + github.com/lestrrat-go/blackmagic v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.0 // indirect + github.com/lestrrat-go/iter v1.0.1 // indirect + github.com/lestrrat-go/option v1.0.0 // indirect +) + require ( github.com/99designs/gqlgen v0.15.1 // indirect github.com/agnivade/levenshtein v1.1.0 // indirect @@ -36,7 +48,8 @@ require ( github.com/json-iterator/go v1.1.9 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/labstack/gommon v0.3.0 // indirect - github.com/leodido/go-urn v1.2.1 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/lestrrat-go/jwx v1.2.20 github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect @@ -52,7 +65,7 @@ require ( github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect - golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect + golang.org/x/crypto v0.0.0-20220214200702-86341886e292 // indirect golang.org/x/net v0.0.0-20211209124913-491a49abca63 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect diff --git a/go.sum b/go.sum index f94b0a9..ab1eac7 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,9 @@ github.com/creasty/defaults v1.5.2/go.mod h1:FPZ+Y0WNrbqOVw+c6av63eyHUAl6pMHZwqL github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d h1:1iy2qD6JEhHKKhUOA9IWs7mjco7lnw2qx8FsRI2wirE= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.0-20210816181553-5444fa50b93d/go.mod h1:tmAIfUFEirG/Y8jhZ9M+h36obRZAk/1fcSpXwAVlfqE= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -72,6 +75,8 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/goccy/go-json v0.9.4 h1:L8MLKG2mvVXiQu07qB6hmfqeSYQdOnqPot2GhsIwIaI= +github.com/goccy/go-json v0.9.4/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -129,8 +134,18 @@ github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0 github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= +github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= +github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4= +github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ= +github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc= +github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE= +github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A= +github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc= +github.com/lestrrat-go/jwx v1.2.20 h1:ckMNlG0MqCcVp7LnD5FN2+459ndm7SW3vryE79Dz9nk= +github.com/lestrrat-go/jwx v1.2.20/go.mod h1:tLE1XszaFgd7zaS5wHe4NxA+XVhu7xgdRvDpNyi3kNM= +github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4= +github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I= github.com/logrusorgru/aurora/v3 v3.0.0/go.mod h1:vsR12bk5grlLvLXAYrBsb5Oc/N+LxAlxggSjiwMnCUc= github.com/matryer/moq v0.2.3/go.mod h1:9RtPYjTnH1bSBIkpvtHkFN7nbWAnO7oRpdJkEIn6UtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -213,10 +228,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292 h1:f+lwQ+GtmgoY+A2YaQxlSOnDjXcQ7ZRLWOHbC6HtRqE= +golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= diff --git a/integrations/kjwtauth/jwtauth.go b/integrations/kjwtauth/jwtauth.go new file mode 100644 index 0000000..b4c20be --- /dev/null +++ b/integrations/kjwtauth/jwtauth.go @@ -0,0 +1,379 @@ +package kjwtauth + +import ( + "context" + "errors" + "net/http" + "strings" + "time" + + "github.com/benbjohnson/clock" + "github.com/gin-gonic/gin" + "github.com/keploy/go-sdk/keploy" + "github.com/labstack/echo/v4" + "github.com/lestrrat-go/jwx/jwa" + "github.com/lestrrat-go/jwx/jwt" +) + +type JWTAuth struct { + alg jwa.SignatureAlgorithm + signKey interface{} // private-key + verifyKey interface{} // public-key, only used by RSA and ECDSA algorithms + verifier jwt.ParseOption + keploy *keploy.Keploy // keploy instace +} + +var ( + TokenCtxKey = &contextKey{"Token"} + ErrorCtxKey = &contextKey{"Error"} + ValidateOptionCtxKey = &contextKey{"ValidateOption"} +) + +var ( + ErrUnauthorized = errors.New("token is unauthorized") + ErrExpired = errors.New("token is expired") + ErrNBFInvalid = errors.New("token nbf validation failed") + ErrIATInvalid = errors.New("token iat validation failed") + ErrNoTokenFound = errors.New("no token found") + ErrAlgoInvalid = errors.New("algorithm mismatch") +) + +func New(alg string, signKey interface{}, verifyKey interface{}, keploy *keploy.Keploy) *JWTAuth { + ja := &JWTAuth{alg: jwa.SignatureAlgorithm(alg), signKey: signKey, verifyKey: verifyKey, keploy: keploy} + + if ja.verifyKey != nil { + ja.verifier = jwt.WithVerify(ja.alg, ja.verifyKey) + } else { + ja.verifier = jwt.WithVerify(ja.alg, ja.signKey) + } + + return ja +} + +func setTestClock(ja *JWTAuth, r *http.Request) jwt.ValidateOption { + id := r.Header.Get("KEPLOY_TEST_ID") + var validateOption jwt.ValidateOption + if id != "" && ja.keploy != nil { + mock := clock.NewMock() + t := ja.keploy.GetClock(id) + mock.Add(time.Duration(t) * time.Second) + validateOption = jwt.WithClock(mock) + } + return validateOption +} + +func setContext(ja *JWTAuth, r *http.Request, findTokenFns ...func(r *http.Request) string) *http.Request { + validateOption := setTestClock(ja, r) + + token, err := VerifyRequest(ja, r, validateOption, findTokenFns...) + ctx := r.Context() + ctx = NewContext(ctx, token, err, validateOption) + return r.WithContext(ctx) +} + +func VerifierEcho(ja *JWTAuth) func(echo.HandlerFunc) echo.HandlerFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return VerifyEcho(ja, TokenFromHeader, TokenFromCookie)(next) + } +} + +func VerifyEcho(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) func(echo.HandlerFunc) echo.HandlerFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(ctx echo.Context) error { + ctx.SetRequest(setContext(ja, ctx.Request(), findTokenFns...)) + return next(ctx) + } + } +} + +func VerifierGin(ja *JWTAuth) gin.HandlerFunc { + return VerifyGin(ja, TokenFromHeader, TokenFromCookie) +} + +func VerifyGin(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) gin.HandlerFunc { + return func(c *gin.Context) { + c.Request = setContext(ja, c.Request, findTokenFns...) + c.Next() + } +} + +// VerifierChi http middleware handler will verify a JWT string from a http request. +// +// Verifier will search for a JWT token in a http request, in the order: +// 1. 'jwt' URI query parameter +// 2. 'Authorization: BEARER T' request header +// 3. Cookie 'jwt' value +// +// The first JWT string that is found as a query parameter, authorization header +// or cookie header is then decoded by the `jwt-go` library and a *jwt.Token +// object is set on the request context. In the case of a signature decoding error +// the Verifier will also set the error on the request context. +// +// The Verifier always calls the next http handler in sequence, which can either +// be the generic `jwtauth.Authenticator` middleware or your own custom handler +// which checks the request context jwt token and error to prepare a custom +// http response. +func VerifierChi(ja *JWTAuth) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return VerifyChi(ja, TokenFromHeader, TokenFromCookie)(next) + } +} + +func VerifyChi(ja *JWTAuth, findTokenFns ...func(r *http.Request) string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + hfn := func(w http.ResponseWriter, r *http.Request) { + r = setContext(ja, r, findTokenFns...) + next.ServeHTTP(w, r) + } + return http.HandlerFunc(hfn) + } +} + +func VerifyRequest(ja *JWTAuth, r *http.Request, validateOption jwt.ValidateOption, findTokenFns ...func(r *http.Request) string) (jwt.Token, error) { + var tokenString string + + // Extract token string from the request by calling token find functions in + // the order they where provided. Further extraction stops if a function + // returns a non-empty string. + for _, fn := range findTokenFns { + tokenString = fn(r) + if tokenString != "" { + break + } + } + if tokenString == "" { + return nil, ErrNoTokenFound + } + + return VerifyToken(ja, tokenString, validateOption) +} + +func VerifyToken(ja *JWTAuth, tokenString string, validateOption jwt.ValidateOption) (jwt.Token, error) { + // Decode & verify the token + token, err := ja.Decode(tokenString) + + if err != nil { + return token, ErrorReason(err) + } + + if token == nil { + return nil, ErrUnauthorized + } + if validateOption == nil { + if err := jwt.Validate(token); err != nil { + return token, ErrorReason(err) + } + return token, nil + } + if err := jwt.Validate(token, validateOption); err != nil { + return token, ErrorReason(err) + } + + // Valid! + return token, nil +} + +func (ja *JWTAuth) Encode(claims map[string]interface{}) (t jwt.Token, tokenString string, err error) { + t = jwt.New() + for k, v := range claims { + t.Set(k, v) + } + payload, err := ja.sign(t) + if err != nil { + return nil, "", err + } + tokenString = string(payload) + return +} + +func (ja *JWTAuth) Decode(tokenString string) (jwt.Token, error) { + return ja.parse([]byte(tokenString)) +} + +func (ja *JWTAuth) sign(token jwt.Token) ([]byte, error) { + return jwt.Sign(token, ja.alg, ja.signKey) +} + +func (ja *JWTAuth) parse(payload []byte) (jwt.Token, error) { + return jwt.Parse(payload, ja.verifier) +} + +// ErrorReason will normalize the error message from the underlining +// jwt library +func ErrorReason(err error) error { + switch err.Error() { + case "exp not satisfied", ErrExpired.Error(): + return ErrExpired + case "iat not satisfied", ErrIATInvalid.Error(): + return ErrIATInvalid + case "nbf not satisfied", ErrNBFInvalid.Error(): + return ErrNBFInvalid + default: + return ErrUnauthorized + } +} + +func authenticateRequest(req *http.Request) string { + token, _, err := FromContext(req.Context()) + if err != nil { + return err.Error() + } + validateOption := GetValidateOption(req.Context()) + if token == nil || (validateOption == nil && jwt.Validate(token) != nil) || (validateOption != nil && jwt.Validate(token, validateOption) != nil) { + return http.StatusText(http.StatusUnauthorized) + } + return "" +} + +func AuthenticatorEcho(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + errStr := authenticateRequest(c.Request()) + if errStr != "" { + c.String(http.StatusUnauthorized, errStr) + return errors.New(errStr) + } + next(c) + return nil + } +} + +func AuthenticatorGin(c *gin.Context) { + errStr := authenticateRequest(c.Request) + if errStr != "" { + c.AbortWithStatusJSON(http.StatusUnauthorized, errStr) + return + } + c.Next() +} + +// AuthenticatorChi is a default authentication middleware to enforce access from the +// Verifier middleware request context values. The Authenticator sends a 401 Unauthorized +// response for any unverified tokens and passes the good ones through. It's just fine +// until you decide to write something similar and customize your client response. +func AuthenticatorChi(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + errStr := authenticateRequest(r) + if errStr != "" { + http.Error(w, errStr, http.StatusUnauthorized) + return + } + next.ServeHTTP(w, r) + }) +} + +func GetValidateOption(ctx context.Context) jwt.ValidateOption { + option, ok := ctx.Value(ValidateOptionCtxKey).(jwt.ValidateOption) + if !ok { + return nil + } + return option +} + +func NewContext(ctx context.Context, t jwt.Token, err error, validateOption jwt.ValidateOption) context.Context { + ctx = context.WithValue(ctx, TokenCtxKey, t) + ctx = context.WithValue(ctx, ErrorCtxKey, err) + ctx = context.WithValue(ctx, ValidateOptionCtxKey, validateOption) + return ctx +} + +func FromContext(ctx context.Context) (jwt.Token, map[string]interface{}, error) { + token, _ := ctx.Value(TokenCtxKey).(jwt.Token) + + var err error + var claims map[string]interface{} + + if token != nil { + claims, err = token.AsMap(context.Background()) + if err != nil { + return token, nil, err + } + } else { + claims = map[string]interface{}{} + } + + err, _ = ctx.Value(ErrorCtxKey).(error) + + return token, claims, err +} + +// UnixTime returns the given time in UTC milliseconds +func UnixTime(tm time.Time) int64 { + return tm.UTC().Unix() +} + +// EpochNow is a helper function that returns the NumericDate time value used by the spec +func EpochNow() int64 { + return time.Now().UTC().Unix() +} + +// ExpireIn is a helper function to return calculated time in the future for "exp" claim +func ExpireIn(tm time.Duration) int64 { + return EpochNow() + int64(tm.Seconds()) +} + +// Set issued at ("iat") to specified time in the claims +func SetIssuedAt(claims map[string]interface{}, tm time.Time) { + claims["iat"] = tm.UTC().Unix() +} + +// Set issued at ("iat") to present time in the claims +func SetIssuedNow(claims map[string]interface{}) { + claims["iat"] = EpochNow() +} + +// Set expiry ("exp") in the claims +func SetExpiry(claims map[string]interface{}, tm time.Time) { + claims["exp"] = tm.UTC().Unix() +} + +// Set expiry ("exp") in the claims to some duration from the present time +func SetExpiryIn(claims map[string]interface{}, tm time.Duration) { + claims["exp"] = ExpireIn(tm) +} + +// TokenFromCookie tries to retreive the token string from a cookie named +// "jwt". +func TokenFromCookie(r *http.Request) string { + cookie, err := r.Cookie("jwt") + if err != nil { + return "" + } + return cookie.Value +} + +// TokenFromHeader tries to retreive the token string from the +// "Authorization" reqeust header: "Authorization: BEARER T". +func TokenFromHeader(r *http.Request) string { + // Get token from authorization header. + bearer := r.Header.Get("Authorization") + if len(bearer) > 7 && strings.ToUpper(bearer[0:6]) == "BEARER" { + return bearer[7:] + } + return "" +} + +// TokenFromQuery tries to retreive the token string from the "jwt" URI +// query parameter. +// +// To use it, build our own middleware handler, such as: +// +// func Verifier(ja *JWTAuth) func(http.Handler) http.Handler { +// return func(next http.Handler) http.Handler { +// return Verify(ja, TokenFromQuery, TokenFromHeader, TokenFromCookie)(next) +// } +// } +func TokenFromQuery(r *http.Request) string { + // Get token from query param named "jwt". + return r.URL.Query().Get("jwt") +} + +// contextKey is a value for use with context.WithValue. It's used as +// a pointer so it fits in an interface{} without allocation. This technique +// for defining context keys was copied from Go 1.7's new use of context in net/http. +type contextKey struct { + name string +} + +func (k *contextKey) String() string { + return "jwtauth context value " + k.name +} diff --git a/keploy/keploy.go b/keploy/keploy.go index 41c269f..81012d3 100644 --- a/keploy/keploy.go +++ b/keploy/keploy.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/creasty/defaults" "github.com/go-playground/validator/v10" + // "github.com/benbjohnson/clock" "go.keploy.io/server/http/regression" "go.keploy.io/server/pkg/models" "go.uber.org/zap" @@ -67,7 +68,7 @@ type AppConfig struct { Port string `validate:"required"` Delay time.Duration `default:"5s"` Timeout time.Duration `default:"60s"` - Filter Filter + Filter Filter } type Filter struct { @@ -109,8 +110,9 @@ func New(cfg Config) *Keploy { client: &http.Client{ Timeout: cfg.App.Timeout, }, - deps: sync.Map{}, - resp: sync.Map{}, + deps: sync.Map{}, + resp: sync.Map{}, + mocktime: sync.Map{}, } if mode == MODE_TEST { go k.Test() @@ -124,8 +126,9 @@ type Keploy struct { client *http.Client deps sync.Map //Deps map[string][]models.Dependency - resp sync.Map + resp sync.Map //Resp map[string]models.HttpResp + mocktime sync.Map } func (k *Keploy) GetDependencies(id string) []models.Dependency { @@ -141,6 +144,20 @@ func (k *Keploy) GetDependencies(id string) []models.Dependency { return deps } +func (k *Keploy) GetClock(id string) int64 { + val, ok := k.mocktime.Load(id) + if !ok { + return 0 + } + mocktime, ok := val.(int64) + if !ok { + k.Log.Error("failed getting time for http request", zap.String("test case id", id)) + return 0 + } + + return mocktime +} + func (k *Keploy) GetResp(id string) models.HttpResp { val, ok := k.resp.Load(id) if !ok { @@ -240,6 +257,12 @@ func (k *Keploy) simulate(tc models.TestCase) (*models.HttpResp, error) { // add dependencies to shared context k.deps.Store(tc.ID, tc.Deps) defer k.deps.Delete(tc.ID) + // mock := clock.NewMock() + // t:=tc.Captured + // mock.Add(time.Duration(t) * time.Second) + // tc.Captured = mock.Now().UTC().Unix() + k.mocktime.Store(tc.ID, tc.Captured) + defer k.mocktime.Delete(tc.ID) //k.Deps[tc.ID] = tc.Deps //defer delete(k.Deps, tc.ID) req, err := http.NewRequest(string(tc.HttpReq.Method), "http://"+k.cfg.App.Host+":"+k.cfg.App.Port+tc.HttpReq.URL, bytes.NewBufferString(tc.HttpReq.Body)) @@ -319,8 +342,8 @@ func (k *Keploy) check(runId string, tc models.TestCase) bool { func (k *Keploy) put(tcs regression.TestCaseReq) { var str = k.cfg.App.Filter - reg:= regexp.MustCompile(str.UrlRegex) - if str.UrlRegex!="" && reg.FindString(tcs.URI) == ""{ + reg := regexp.MustCompile(str.UrlRegex) + if str.UrlRegex != "" && reg.FindString(tcs.URI) == "" { return } @@ -362,14 +385,14 @@ func (k *Keploy) put(tcs regression.TestCaseReq) { } id := res["id"] if id == "" { - return + return } k.denoise(id, tcs) } -func (k *Keploy) denoise (id string, tcs regression.TestCaseReq){ +func (k *Keploy) denoise(id string, tcs regression.TestCaseReq) { // run the request again to find noisy fields - time.Sleep(2*time.Second) + time.Sleep(2 * time.Second) resp2, err := k.simulate(models.TestCase{ ID: id, Captured: tcs.Captured, diff --git a/keploy/utils.go b/keploy/utils.go index b03a1c0..0bbae7b 100644 --- a/keploy/utils.go +++ b/keploy/utils.go @@ -163,7 +163,7 @@ func CaptureTestcase(k *Keploy, r *http.Request, reqBody []byte, resp models.Htt HttpResp: resp, Deps: deps.Deps, }) - + } func urlParams(r *http.Request, params map[string]string) map[string]string {