Skip to content

Commit 516dd45

Browse files
authored
Merge pull request #163 from checkr/zz/correctly-handle-401-jwt
Fix 401 jwt middleware handling
2 parents 42ae967 + 323e7ea commit 516dd45

File tree

4 files changed

+98
-28
lines changed

4 files changed

+98
-28
lines changed

pkg/config/env.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,15 @@ var Config = struct {
107107
Note:
108108
If the access_token is present in both the header and cookie only the latest will be used
109109
*/
110-
JWTAuthEnabled bool `env:"FLAGR_JWT_AUTH_ENABLED" envDefault:"false"`
111-
JWTAuthDebug bool `env:"FLAGR_JWT_AUTH_DEBUG" envDefault:"false"`
112-
JWTAuthWhitelistPaths []string `env:"FLAGR_JWT_AUTH_WHITELIST_PATHS" envDefault:"/api/v1/evaluation" envSeparator:","`
113-
JWTAuthCookieTokenName string `env:"FLAGR_JWT_AUTH_COOKIE_TOKEN_NAME" envDefault:"access_token"`
114-
JWTAuthSecret string `env:"FLAGR_JWT_AUTH_SECRET" envDefault:""`
115-
JWTAuthNoTokenStatusCode int `env:"FLAGR_JWT_AUTH_NO_TOKEN_STATUS_CODE" envDefault:"307"` // "307" or "401"
116-
JWTAuthNoTokenRedirectURL string `env:"FLAGR_JWT_AUTH_NO_TOKEN_REDIRECT_URL" envDefault:""`
117-
JWTAuthUserProperty string `env:"FLAGR_JWT_AUTH_USER_PROPERTY" envDefault:"flagr_user"`
110+
JWTAuthEnabled bool `env:"FLAGR_JWT_AUTH_ENABLED" envDefault:"false"`
111+
JWTAuthDebug bool `env:"FLAGR_JWT_AUTH_DEBUG" envDefault:"false"`
112+
JWTAuthPrefixWhitelistPaths []string `env:"FLAGR_JWT_AUTH_WHITELIST_PATHS" envDefault:"/api/v1/evaluation,/static" envSeparator:","`
113+
JWTAuthExactWhitelistPaths []string `env:"FLAGR_JWT_AUTH_EXACT_WHITELIST_PATHS" envDefault:",/" envSeparator:","`
114+
JWTAuthCookieTokenName string `env:"FLAGR_JWT_AUTH_COOKIE_TOKEN_NAME" envDefault:"access_token"`
115+
JWTAuthSecret string `env:"FLAGR_JWT_AUTH_SECRET" envDefault:""`
116+
JWTAuthNoTokenStatusCode int `env:"FLAGR_JWT_AUTH_NO_TOKEN_STATUS_CODE" envDefault:"307"` // "307" or "401"
117+
JWTAuthNoTokenRedirectURL string `env:"FLAGR_JWT_AUTH_NO_TOKEN_REDIRECT_URL" envDefault:""`
118+
JWTAuthUserProperty string `env:"FLAGR_JWT_AUTH_USER_PROPERTY" envDefault:"flagr_user"`
118119

119120
// "HS256" and "RS256" supported
120121
JWTAuthSigningMethod string `env:"FLAGR_JWT_AUTH_SIGNING_METHOD" envDefault:"HS256"`

pkg/config/middleware.go

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ func setupJWTAuthMiddleware() *auth {
8080
}
8181

8282
return &auth{
83-
WhitelistPaths: Config.JWTAuthWhitelistPaths,
83+
PrefixWhitelistPaths: Config.JWTAuthPrefixWhitelistPaths,
84+
ExactWhitelistPaths: Config.JWTAuthExactWhitelistPaths,
8485
JWTMiddleware: jwtmiddleware.New(jwtmiddleware.Options{
8586
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
8687
return validationKey, errParsingKey
@@ -116,18 +117,36 @@ func jwtErrorHandler(w http.ResponseWriter, r *http.Request, err string) {
116117
}
117118

118119
type auth struct {
119-
WhitelistPaths []string
120-
JWTMiddleware *jwtmiddleware.JWTMiddleware
120+
PrefixWhitelistPaths []string
121+
ExactWhitelistPaths []string
122+
JWTMiddleware *jwtmiddleware.JWTMiddleware
121123
}
122124

123-
func (a *auth) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
125+
func (a *auth) whitelist(req *http.Request) bool {
124126
path := req.URL.Path
125-
for _, p := range a.WhitelistPaths {
127+
128+
// If we set to 401 unauthorized, let the client handles the 401 itself
129+
if Config.JWTAuthNoTokenStatusCode == http.StatusUnauthorized {
130+
for _, p := range a.ExactWhitelistPaths {
131+
if p == path {
132+
return true
133+
}
134+
}
135+
}
136+
137+
for _, p := range a.PrefixWhitelistPaths {
126138
if p != "" && strings.HasPrefix(path, p) {
127-
next(w, req)
128-
return
139+
return true
129140
}
130141
}
142+
return false
143+
}
144+
145+
func (a *auth) ServeHTTP(w http.ResponseWriter, req *http.Request, next http.HandlerFunc) {
146+
if a.whitelist(req) {
147+
next(w, req)
148+
return
149+
}
131150
a.JWTMiddleware.HandlerWithNext(w, req, next)
132151
}
133152

pkg/config/middleware_test.go

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func TestAuthMiddleware(t *testing.T) {
113113

114114
res := httptest.NewRecorder()
115115
res.Body = new(bytes.Buffer)
116-
req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:18000%s", Config.JWTAuthWhitelistPaths[0]), nil)
116+
req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:18000%s", Config.JWTAuthPrefixWhitelistPaths[0]), nil)
117117
hh.ServeHTTP(res, req)
118118
assert.Equal(t, http.StatusOK, res.Code)
119119
})
@@ -233,6 +233,67 @@ o2kQ+X5xK9cipRgEKwIDAQAB
233233
})
234234
}
235235

236+
func TestAuthMiddlewareWithUnauthorized(t *testing.T) {
237+
h := &okHandler{}
238+
239+
t.Run("it will return 401 if no cookie passed", func(t *testing.T) {
240+
Config.JWTAuthEnabled = true
241+
Config.JWTAuthNoTokenStatusCode = http.StatusUnauthorized
242+
defer func() {
243+
Config.JWTAuthEnabled = false
244+
Config.JWTAuthNoTokenStatusCode = http.StatusTemporaryRedirect
245+
}()
246+
247+
hh := SetupGlobalMiddleware(h)
248+
res := httptest.NewRecorder()
249+
res.Body = new(bytes.Buffer)
250+
req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil)
251+
hh.ServeHTTP(res, req)
252+
assert.Equal(t, http.StatusUnauthorized, res.Code)
253+
})
254+
255+
t.Run("it will return 200 if cookie passed", func(t *testing.T) {
256+
Config.JWTAuthEnabled = true
257+
Config.JWTAuthNoTokenStatusCode = http.StatusUnauthorized
258+
defer func() {
259+
Config.JWTAuthEnabled = false
260+
Config.JWTAuthNoTokenStatusCode = http.StatusTemporaryRedirect
261+
}()
262+
263+
hh := SetupGlobalMiddleware(h)
264+
res := httptest.NewRecorder()
265+
res.Body = new(bytes.Buffer)
266+
req, _ := http.NewRequest("GET", "http://localhost:18000/api/v1/flags", nil)
267+
req.AddCookie(&http.Cookie{
268+
Name: "access_token",
269+
Value: validHS256JWTToken,
270+
})
271+
hh.ServeHTTP(res, req)
272+
assert.Equal(t, http.StatusOK, res.Code)
273+
})
274+
275+
t.Run("it will return 200 for some paths", func(t *testing.T) {
276+
Config.JWTAuthEnabled = true
277+
Config.JWTAuthNoTokenStatusCode = http.StatusUnauthorized
278+
defer func() {
279+
Config.JWTAuthEnabled = false
280+
Config.JWTAuthNoTokenStatusCode = http.StatusTemporaryRedirect
281+
}()
282+
283+
testPaths := []string{"/", "", "/#", "/#/", "/static", "/static/"}
284+
for _, path := range testPaths {
285+
t.Run(fmt.Sprintf("path: %s", path), func(t *testing.T) {
286+
hh := SetupGlobalMiddleware(h)
287+
res := httptest.NewRecorder()
288+
res.Body = new(bytes.Buffer)
289+
req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:18000%s", path), nil)
290+
hh.ServeHTTP(res, req)
291+
assert.Equal(t, http.StatusOK, res.Code)
292+
})
293+
}
294+
})
295+
}
296+
236297
func TestStatsMiddleware(t *testing.T) {
237298
t.Run("it will setup statsd if statsd is enabled", func(t *testing.T) {
238299
Config.StatsdEnabled = true

pkg/handler/eval_cache_test.go

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,14 @@ import (
99
"github.com/stretchr/testify/assert"
1010
)
1111

12-
func TestGetEvalCacheStart(t *testing.T) {
13-
db := entity.PopulateTestDB(entity.GenFixtureFlag())
14-
defer db.Close()
15-
defer gostub.StubFunc(&getDB, db).Reset()
16-
17-
ec := GetEvalCache()
18-
assert.NotPanics(t, func() {
19-
ec.Start()
20-
})
21-
}
22-
2312
func TestGetByFlagKeyOrID(t *testing.T) {
2413
fixtureFlag := entity.GenFixtureFlag()
2514
db := entity.PopulateTestDB(fixtureFlag)
2615
defer db.Close()
2716
defer gostub.StubFunc(&getDB, db).Reset()
2817

2918
ec := GetEvalCache()
30-
ec.Start()
19+
ec.reloadMapCache()
3120
f := ec.GetByFlagKeyOrID(fixtureFlag.ID)
3221
assert.Equal(t, f.ID, fixtureFlag.ID)
3322
}

0 commit comments

Comments
 (0)