diff --git a/pkg/config/env.go b/pkg/config/env.go index 214f42dc..4b2a29b2 100644 --- a/pkg/config/env.go +++ b/pkg/config/env.go @@ -79,10 +79,24 @@ var Config = struct { RecorderKafkaEncrypted bool `env:"FLAGR_RECORDER_KAFKA_ENCRYPTED" envDefault:"false"` RecorderKafkaEncryptionKey string `env:"FLAGR_RECORDER_KAFKA_ENCRYPTION_KEY" envDefault:""` - // JWTAuthEnabled enables the JWT Auth - // The pattern of using JWT auth token is that it redirects to the URL to set cross subdomain cookie - // For example, redirect to auth.example.com/signin, which sets Cookie access_token=jwt_token for domain ".example.com" - // One can also whitelist some routes so that they don't get blocked by JWT auth + /** + JWTAuthEnabled enables the JWT Auth + + Via Cookies: + The pattern of using JWT auth token using cookies is that it redirects to the URL to set cross subdomain cookie + For example, redirect to auth.example.com/signin, which sets Cookie access_token=jwt_token for domain + ".example.com". One can also whitelist some routes so that they don't get blocked by JWT auth + + Via Headers: + If you wish to use JWT Auth via headers you can simply set the header `Authorization Bearer [access_token]` + + Supported signing methods: + * HS256, in this case `FLAGR_JWT_AUTH_SECRET` contains the passphrase + * RS256, in this case `FLAGR_JWT_AUTH_SECRET` contains the key in PEM Format + + Note: + If the access_token is present in both the header and cookie only the latest will be used + */ JWTAuthEnabled bool `env:"FLAGR_JWT_AUTH_ENABLED" envDefault:"false"` JWTAuthDebug bool `env:"FLAGR_JWT_AUTH_DEBUG" envDefault:"false"` JWTAuthWhitelistPaths []string `env:"FLAGR_JWT_AUTH_WHITELIST_PATHS" envDefault:"/api/v1/evaluation" envSeparator:","` @@ -90,4 +104,7 @@ var Config = struct { JWTAuthSecret string `env:"FLAGR_JWT_AUTH_SECRET" envDefault:""` JWTAuthNoTokenRedirectURL string `env:"FLAGR_JWT_AUTH_NO_TOKEN_REDIRECT_URL" envDefault:""` JWTAuthUserProperty string `env:"FLAGR_JWT_AUTH_USER_PROPERTY" envDefault:"flagr_user"` + + // "HS256" and "RS256" supported + JWTAuthSigningMethod string `env:"FLAGR_JWT_AUTH_SIGNING_METHOD" envDefault:"HS256"` }{} diff --git a/pkg/config/middleware.go b/pkg/config/middleware.go index be4b3d76..d1ac0d90 100644 --- a/pkg/config/middleware.go +++ b/pkg/config/middleware.go @@ -8,14 +8,14 @@ import ( "time" "github.com/DataDog/datadog-go/statsd" - jwtmiddleware "github.com/auth0/go-jwt-middleware" + "github.com/auth0/go-jwt-middleware" "github.com/dgrijalva/jwt-go" "github.com/gohttp/pprof" - negronilogrus "github.com/meatballhat/negroni-logrus" + "github.com/meatballhat/negroni-logrus" "github.com/rs/cors" "github.com/sirupsen/logrus" "github.com/urfave/negroni" - negroninewrelic "github.com/yadvendar/negroni-newrelic-go-agent" + "github.com/yadvendar/negroni-newrelic-go-agent" ) // SetupGlobalMiddleware setup the global middleware @@ -40,27 +40,7 @@ func SetupGlobalMiddleware(handler http.Handler) http.Handler { } if Config.JWTAuthEnabled { - n.Use(&auth{ - WhitelistPaths: Config.JWTAuthWhitelistPaths, - JWTMiddleware: jwtmiddleware.New(jwtmiddleware.Options{ - ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { - return []byte(Config.JWTAuthSecret), nil - }, - SigningMethod: jwt.SigningMethodHS256, - Extractor: func(r *http.Request) (string, error) { - c, err := r.Cookie(Config.JWTAuthCookieTokenName) - if err != nil { - return "", err - } - return c.Value, nil - }, - UserProperty: Config.JWTAuthUserProperty, - Debug: Config.JWTAuthDebug, - ErrorHandler: func(w http.ResponseWriter, r *http.Request, err string) { - http.Redirect(w, r, Config.JWTAuthNoTokenRedirectURL, http.StatusTemporaryRedirect) - }, - }), - }) + n.Use(setupJWTAuthMiddleware()) } if Config.MiddlewareVerboseLoggerEnabled { @@ -79,6 +59,52 @@ func SetupGlobalMiddleware(handler http.Handler) http.Handler { return n } +/** +setupJWTAuthMiddleware setup an JWTMiddleware from the ENV config +*/ +func setupJWTAuthMiddleware() *auth { + var signingMethod jwt.SigningMethod + var validationKey interface{} + var errParsingKey error + + switch Config.JWTAuthSigningMethod { + case "HS256": + signingMethod = jwt.SigningMethodHS256 + validationKey = []byte(Config.JWTAuthSecret) + case "RS256": + signingMethod = jwt.SigningMethodRS256 + validationKey, errParsingKey = jwt.ParseRSAPublicKeyFromPEM([]byte(Config.JWTAuthSecret)) + default: + signingMethod = jwt.SigningMethodHS256 + validationKey = []byte("") + } + + return &auth{ + WhitelistPaths: Config.JWTAuthWhitelistPaths, + JWTMiddleware: jwtmiddleware.New(jwtmiddleware.Options{ + ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) { + return validationKey, errParsingKey + }, + SigningMethod: signingMethod, + Extractor: jwtmiddleware.FromFirst( + func(r *http.Request) (string, error) { + c, err := r.Cookie(Config.JWTAuthCookieTokenName) + if err != nil { + return "", nil + } + return c.Value, nil + }, + jwtmiddleware.FromAuthHeader, + ), + UserProperty: Config.JWTAuthUserProperty, + Debug: Config.JWTAuthDebug, + ErrorHandler: func(w http.ResponseWriter, r *http.Request, err string) { + http.Redirect(w, r, Config.JWTAuthNoTokenRedirectURL, http.StatusTemporaryRedirect) + }, + }), + } +} + type auth struct { WhitelistPaths []string JWTMiddleware *jwtmiddleware.JWTMiddleware diff --git a/pkg/config/middleware_test.go b/pkg/config/middleware_test.go index e9e909c1..b1365be3 100644 --- a/pkg/config/middleware_test.go +++ b/pkg/config/middleware_test.go @@ -15,6 +15,23 @@ import ( type okHandler struct{} +const ( + // Signed with secret: "" + validHS256JWTToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmbGFncl91c2VyIjoiMTIzNDU2Nzg5MCJ9.CLXgNEtwPCqCOtUU-KmqDyO8S2wC_G6PZ0tml8DCuNw" + + // Public Key: + //-----BEGIN PUBLIC KEY----- + //MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd + //UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs + //HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D + //o2kQ+X5xK9cipRgEKwIDAQAB + //-----END PUBLIC KEY----- + validRS256JWTToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TCYt5XsITJX1CxPCT8yAV-TVkIEq_PbChOMqsLfRoPsnsgw5WEuts01mq-pQy7UJiN5mgRxD-WUcX16dUEMGlv50aqzpqh4Qktb3rk-BuQy72IFLOqV0G_zS245-kronKb78cPN25DGlcTwLtjPAYuNzVBAh4vGHSrQyHUdBBPM" + + // Signed with secret: "mysecret" + validHS256JWTTokenWithSecret = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.drt_po6bHhDOF_FJEHTrK-KD8OGjseJZpHwHIgsnoTM" +) + func (o *okHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { w.Write([]byte("OK")) } @@ -83,7 +100,7 @@ func TestAuthMiddleware(t *testing.T) { req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) req.AddCookie(&http.Cookie{ Name: "access_token", - Value: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmbGFncl91c2VyIjoiMTIzNDU2Nzg5MCJ9.CLXgNEtwPCqCOtUU-KmqDyO8S2wC_G6PZ0tml8DCuNw", // {"flagr_user": "1234567890"} + Value: validHS256JWTToken, }) hh.ServeHTTP(res, req) assert.Equal(t, http.StatusOK, res.Code) @@ -100,6 +117,120 @@ func TestAuthMiddleware(t *testing.T) { hh.ServeHTTP(res, req) assert.Equal(t, http.StatusOK, res.Code) }) + + t.Run("it will pass if jwt enabled with correct header token", func(t *testing.T) { + Config.JWTAuthEnabled = true + defer func() { Config.JWTAuthEnabled = false }() + hh := SetupGlobalMiddleware(h) + + res := httptest.NewRecorder() + res.Body = new(bytes.Buffer) + req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) + req.Header.Add("Authorization", "Bearer "+validHS256JWTToken) + hh.ServeHTTP(res, req) + assert.Equal(t, http.StatusOK, res.Code) + }) + + t.Run("it will redirect if jwt enabled with invalid cookie token and valid header token", func(t *testing.T) { + Config.JWTAuthEnabled = true + defer func() { Config.JWTAuthEnabled = false }() + hh := SetupGlobalMiddleware(h) + + res := httptest.NewRecorder() + res.Body = new(bytes.Buffer) + req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) + req.AddCookie(&http.Cookie{ + Name: "access_token", + Value: "invalid_jwt", + }) + req.Header.Add("Authorization", "Bearer "+validHS256JWTToken) + hh.ServeHTTP(res, req) + assert.Equal(t, http.StatusTemporaryRedirect, res.Code) + }) + + t.Run("it will redirect if jwt enabled and a cookie token encrypted with the wrong method", func(t *testing.T) { + Config.JWTAuthEnabled = true + Config.JWTAuthSigningMethod = "RS256" + defer func() { + Config.JWTAuthEnabled = false + Config.JWTAuthSigningMethod = "HS256" + }() + hh := SetupGlobalMiddleware(h) + + res := httptest.NewRecorder() + res.Body = new(bytes.Buffer) + req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) + req.AddCookie(&http.Cookie{ + Name: "access_token", + Value: "invalid_jwt", + }) + hh.ServeHTTP(res, req) + assert.Equal(t, http.StatusTemporaryRedirect, res.Code) + }) + + t.Run("it will pass if jwt enabled with correct header token encrypted using RS256", func(t *testing.T) { + Config.JWTAuthEnabled = true + Config.JWTAuthSigningMethod = "RS256" + Config.JWTAuthSecret = `-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd +UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs +HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D +o2kQ+X5xK9cipRgEKwIDAQAB +-----END PUBLIC KEY-----` + defer func() { + Config.JWTAuthEnabled = false + Config.JWTAuthSigningMethod = "HS256" + Config.JWTAuthSecret = "" + }() + hh := SetupGlobalMiddleware(h) + + res := httptest.NewRecorder() + res.Body = new(bytes.Buffer) + req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) + req.Header.Add("Authorization", "Bearer "+validRS256JWTToken) + hh.ServeHTTP(res, req) + assert.Equal(t, http.StatusOK, res.Code) + }) + + t.Run("it will pass if jwt enabled with valid cookie token with passphrase", func(t *testing.T) { + Config.JWTAuthEnabled = true + Config.JWTAuthSecret = "mysecret" + defer func() { + Config.JWTAuthEnabled = false + Config.JWTAuthSecret = "" + }() + hh := SetupGlobalMiddleware(h) + + res := httptest.NewRecorder() + res.Body = new(bytes.Buffer) + req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) + req.AddCookie(&http.Cookie{ + Name: "access_token", + Value: validHS256JWTTokenWithSecret, + }) + hh.ServeHTTP(res, req) + assert.Equal(t, http.StatusOK, res.Code) + }) + + t.Run("it will pass with a correct HS256 token cookie when signing method is wrong and it defaults to empty string secret", func(t *testing.T) { + Config.JWTAuthEnabled = true + Config.JWTAuthSigningMethod = "invalid" + defer func() { + Config.JWTAuthEnabled = false + Config.JWTAuthSigningMethod = "HS256" + }() + hh := SetupGlobalMiddleware(h) + + res := httptest.NewRecorder() + res.Body = new(bytes.Buffer) + req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil) + req.AddCookie(&http.Cookie{ + Name: "access_token", + Value: validHS256JWTToken, + }) + hh.ServeHTTP(res, req) + assert.Equal(t, http.StatusOK, res.Code) + }) } func TestStatsMiddleware(t *testing.T) {