Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add JWT auth via headers and RS256 signing option #146

Merged
merged 2 commits into from
Jul 23, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,32 @@ 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:","`
JWTAuthCookieTokenName string `env:"FLAGR_JWT_AUTH_COOKIE_TOKEN_NAME" envDefault:"access_token"`
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"`
}{}
74 changes: 50 additions & 24 deletions pkg/config/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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
Expand Down
133 changes: 132 additions & 1 deletion pkg/config/middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down