Skip to content

Commit

Permalink
Merge pull request #146 from vayan/add-header-jwt-and-rs256-token
Browse files Browse the repository at this point in the history
Add JWT auth via headers and RS256 signing option
  • Loading branch information
zhouzhuojie authored Jul 23, 2018
2 parents f060319 + f6e98bf commit a37ab86
Show file tree
Hide file tree
Showing 3 changed files with 203 additions and 29 deletions.
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

0 comments on commit a37ab86

Please sign in to comment.