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 1 commit
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
1 change: 1 addition & 0 deletions pkg/config/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,5 @@ 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"`
JWTAuthSigningMethod string `env:"FLAGR_JWT_AUTH_SIGNING_METHOD" envDefault:"HS256"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a comment for possible values of signing methods.

The whole file will be served as the doc at https://checkr.github.io/flagr/#/flagr_env

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, can you help change the comments above of the pattern of how to use JWT auth?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I missed this comment 👍

}{}
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 if jwt enabled with correct HS256 token cookie and signing method is wrong", func(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: change to "it will pass with a correct HS256 token cookie when signing method is wrong and it defaults to empty string secret"

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