Skip to content

Commit 7b34924

Browse files
simonpahleshepelyuk
authored andcommitted
feat: add source config of the JWT bearer/header/cookie/query
The config field is called JwtSources, the format is [{type: ...., key: ...},] Possible types are bearer, header, cookie, query. The order of the list enty is the order in wich the JWT wil be tried to retrieved.
1 parent 12a6076 commit 7b34924

File tree

3 files changed

+120
-23
lines changed

3 files changed

+120
-23
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ JwtHeaders | Map used to inject JWT payload fields as HTTP request headers.
5858
OpaHeaders | Map used to inject OPA result fields as HTTP request headers. Populated if request is allowed by OPA. Only 1st level keys from OPA document are supported.
5959
OpaResponseHeaders | Map used to inject OPA result fields as HTTP response headers. Populated if OPA response has `OpaAllowField` present, regardless of value. Only 1st level keys from OPA document are supported.
6060
OpaHttpStatusField | Field in OPA JSON result, which contains int or string HTTP status code that will be returned in case of disallowed OPA response. Accepted range is >= 300 and < 600. Only 1st level keys from OPA document are supported.
61-
JwtCookieKey | Name of the cookie to extract JWT if not found in `Authorization` header.
62-
JwtQueryKey | Name of the query parameter to extract JWT if not found in `Authorization` header or in the specified cookie.
61+
JwtCookieKey | (Deprecated, use JwtSources)Name of the cookie to extract JWT if not found in `Authorization` header.
62+
JwtQueryKey | (Deprecated, use JwtSources) Name of the query parameter to extract JWT if not found in `Authorization` header or in the specified cookie.
63+
JwtSources | Ordered List of sources [bearer, header, query, cookie] of the JWT. config format is a list of maps e.g. [{type: bearer, key: Authorization}, {type: query, key, jwt}]
64+
6365

6466
### Example configuration
6567

@@ -98,7 +100,11 @@ spec:
98100
OpaResponseHeaders:
99101
X-Allowed: allow
100102
OpaHttpStatusField: allow_status_code
101-
JwtCookieKey: jwt
103+
JwtSources:
104+
- type: bearer
105+
key: Authorization
106+
- type: cookie
107+
key: jwt
102108
---
103109
apiVersion: networking.k8s.io/v1
104110
kind: Ingress

jwt.go

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,9 @@ type Config struct {
4646
JwksHeaders map[string]string
4747
OpaResponseHeaders map[string]string
4848
OpaHttpStatusField string
49-
JwtCookieKey string
50-
JwtQueryKey string
49+
JwtCookieKey string // Deprecated: use JwtSources instead
50+
JwtQueryKey string // Deprecated: use JwtSources instead
51+
JwtSources []map[string]string
5152
}
5253

5354
// CreateConfig creates a new OPA Config
@@ -77,8 +78,7 @@ type JwtPlugin struct {
7778
jwksHeaders map[string]string
7879
opaResponseHeaders map[string]string
7980
opaHttpStatusField string
80-
jwtCookieKey string
81-
jwtQueryKey string
81+
jwtSources []map[string]string
8282

8383
name string
8484
keysLock sync.RWMutex
@@ -191,10 +191,19 @@ func New(ctx context.Context, next http.Handler, config *Config, pluginName stri
191191
jwksHeaders: config.JwksHeaders,
192192
opaResponseHeaders: config.OpaResponseHeaders,
193193
opaHttpStatusField: config.OpaHttpStatusField,
194-
jwtCookieKey: config.JwtCookieKey,
195-
jwtQueryKey: config.JwtQueryKey,
194+
jwtSources: config.JwtSources,
196195
name: pluginName,
197196
}
197+
// use default order if jwtSourceOrder is set
198+
if len(jwtPlugin.jwtSources) == 0 {
199+
jwtPlugin.jwtSources = []map[string]string{{"type": "bearer", "key": "Authorization"}}
200+
if config.JwtCookieKey != "" {
201+
jwtPlugin.jwtSources = append(jwtPlugin.jwtSources, map[string]string{"type": "cookie", "key": config.JwtCookieKey})
202+
}
203+
if config.JwtQueryKey != "" {
204+
jwtPlugin.jwtSources = append(jwtPlugin.jwtSources, map[string]string{"type": "query", "key": config.JwtQueryKey})
205+
}
206+
}
198207
if len(config.Keys) > 0 {
199208
if err := jwtPlugin.ParseKeys(config.Keys); err != nil {
200209
return nil, err
@@ -486,13 +495,33 @@ func (jwtPlugin *JwtPlugin) CheckToken(request *http.Request, rw http.ResponseWr
486495
}
487496

488497
func (jwtPlugin *JwtPlugin) ExtractToken(request *http.Request) (*JWT, error) {
489-
// first check if the token is present in header and is valid
490-
jwtTokenStr, err := jwtPlugin.extractTokenFromHeader(request)
491-
if err != nil && jwtPlugin.jwtCookieKey != "" {
492-
jwtTokenStr, err = jwtPlugin.extractTokenFromCookie(request)
493-
}
494-
if err != nil && jwtPlugin.jwtQueryKey != "" {
495-
jwtTokenStr, err = jwtPlugin.extractTokenFromQuery(request)
498+
// extract from header, cookie, or query with given priority
499+
var jwtTokenStr string
500+
var err error
501+
for _, sourceconfig := range jwtPlugin.jwtSources {
502+
sourcetype, oktype := sourceconfig["type"]
503+
if !oktype || (sourcetype != "bearer" && sourcetype != "header" && sourcetype != "cookie" && sourcetype != "query") {
504+
jwtTokenStr, err = "", fmt.Errorf("source type unknown")
505+
continue
506+
}
507+
sourcekey, okkey := sourceconfig["key"]
508+
if !okkey || sourcekey == "" {
509+
jwtTokenStr, err = "", fmt.Errorf("source key not found or empty")
510+
continue
511+
}
512+
switch sourcetype {
513+
case "bearer":
514+
jwtTokenStr, err = jwtPlugin.extractTokenFromBearer(request, sourcekey)
515+
case "header":
516+
jwtTokenStr, err = jwtPlugin.extractTokenFromHeader(request, sourcekey)
517+
case "cookie":
518+
jwtTokenStr, err = jwtPlugin.extractTokenFromCookie(request, sourcekey)
519+
case "query":
520+
jwtTokenStr, err = jwtPlugin.extractTokenFromQuery(request, sourcekey)
521+
}
522+
if err == nil && jwtTokenStr != "" {
523+
break
524+
}
496525
}
497526
if err != nil {
498527
return nil, err
@@ -535,32 +564,40 @@ func (jwtPlugin *JwtPlugin) ExtractToken(request *http.Request) (*JWT, error) {
535564
return &jwtToken, nil
536565
}
537566

538-
func (jwtPlugin *JwtPlugin) extractTokenFromHeader(request *http.Request) (string, error) {
539-
authHeader, ok := request.Header["Authorization"]
567+
func (jwtPlugin *JwtPlugin) extractTokenFromHeader(request *http.Request, key string) (string, error) {
568+
authHeader, ok := request.Header[key]
540569
if !ok {
541570
return "", fmt.Errorf("authorization header missing")
542571
}
543572
auth := authHeader[0]
573+
return auth, nil
574+
}
575+
576+
func (jwtPlugin *JwtPlugin) extractTokenFromBearer(request *http.Request, key string) (string, error) {
577+
auth, err := jwtPlugin.extractTokenFromHeader(request, key)
578+
if err != nil {
579+
return auth, err
580+
}
544581
if !strings.HasPrefix(strings.ToLower(auth), "bearer ") {
545582
return "", fmt.Errorf("authorization type not Bearer")
546583
}
547584
return auth[7:], nil
548585
}
549586

550-
func (jwtPlugin *JwtPlugin) extractTokenFromCookie(request *http.Request) (string, error) {
551-
cookie, err := request.Cookie(jwtPlugin.jwtCookieKey)
587+
func (jwtPlugin *JwtPlugin) extractTokenFromCookie(request *http.Request, key string) (string, error) {
588+
cookie, err := request.Cookie(key)
552589
if err != nil {
553590
return "", err
554591
}
555592
return cookie.Value, nil
556593
}
557594

558-
func (jwtPlugin *JwtPlugin) extractTokenFromQuery(request *http.Request) (string, error) {
595+
func (jwtPlugin *JwtPlugin) extractTokenFromQuery(request *http.Request, key string) (string, error) {
559596
query := request.URL.Query()
560-
if !query.Has(jwtPlugin.jwtQueryKey) {
597+
if !query.Has(key) {
561598
return "", fmt.Errorf("query parameter missing")
562599
}
563-
parameter := query.Get(jwtPlugin.jwtQueryKey)
600+
parameter := query.Get(key)
564601
return parameter, nil
565602
}
566603

jwt_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,60 @@ func TestTokenFromQueryConfiguredButNotInURL(t *testing.T) {
11931193
}
11941194
}
11951195

1196+
func TestTokenFromHeaderConfigured(t *testing.T) {
1197+
cfg := *CreateConfig()
1198+
cfg.JwtSources = []map[string]string{{"type": "header", "key": "X-JWT"}}
1199+
ctx := context.Background()
1200+
nextCalled := false
1201+
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true })
1202+
1203+
jwt, err := New(ctx, next, &cfg, "test-traefik-jwt-plugin")
1204+
if err != nil {
1205+
t.Fatal(err)
1206+
}
1207+
1208+
recorder := httptest.NewRecorder()
1209+
1210+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
1211+
if err != nil {
1212+
t.Fatal(err)
1213+
}
1214+
req.Header["X-JWT"] = []string{"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TnHVsM5_N0SKi_HCwlz3ys1cDktu10g_sKkjqzVe5k09z-bmByflWPFWjAbwgRCKAc77kF8BjDNv0gisAPurBxgxNGxioDFehhcb0IS0YeCAWpzRfBMT6gQZ1gZeNM2Dg_yf4shPhF4rcUCGqnFFzIDSU9Rv2NNMK5DPO4512uTxAQUMHpi5PGTki-zykqTB10Ju1L4jRhmJwJDtGcfdHPlEKKUrFPfYl3RPZLOfdyAqSJ8Gi0R3ymDffmXHz08AJUAY_Kapk8laggIYcvFJhYGJBWZpcy7NWMiOIjEI3bogki4o7z0-Z1xMZdZ9rqypQ1MB44F8VZS2KkPfEmhSog"}
1215+
1216+
jwt.ServeHTTP(recorder, req)
1217+
1218+
if nextCalled == false {
1219+
t.Fatal("next.ServeHTTP was not called")
1220+
}
1221+
}
1222+
1223+
func TestTokenSourceOrder(t *testing.T) {
1224+
cfg := *CreateConfig()
1225+
cfg.JwtSources = []map[string]string{{"type": "header", "key": "X-JWT"}, {"type": "cookie", "key": "jwt"}}
1226+
ctx := context.Background()
1227+
nextCalled := false
1228+
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true })
1229+
1230+
jwt, err := New(ctx, next, &cfg, "test-traefik-jwt-plugin")
1231+
if err != nil {
1232+
t.Fatal(err)
1233+
}
1234+
1235+
recorder := httptest.NewRecorder()
1236+
1237+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
1238+
if err != nil {
1239+
t.Fatal(err)
1240+
}
1241+
req.AddCookie(&http.Cookie{Name: "jwt", Value: "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TnHVsM5_N0SKi_HCwlz3ys1cDktu10g_sKkjqzVe5k09z-bmByflWPFWjAbwgRCKAc77kF8BjDNv0gisAPurBxgxNGxioDFehhcb0IS0YeCAWpzRfBMT6gQZ1gZeNM2Dg_yf4shPhF4rcUCGqnFFzIDSU9Rv2NNMK5DPO4512uTxAQUMHpi5PGTki-zykqTB10Ju1L4jRhmJwJDtGcfdHPlEKKUrFPfYl3RPZLOfdyAqSJ8Gi0R3ymDffmXHz08AJUAY_Kapk8laggIYcvFJhYGJBWZpcy7NWMiOIjEI3bogki4o7z0-Z1xMZdZ9rqypQ1MB44F8VZS2KkPfEmhSog"})
1242+
1243+
jwt.ServeHTTP(recorder, req)
1244+
1245+
if nextCalled == false {
1246+
t.Fatal("next.ServeHTTP was not called")
1247+
}
1248+
}
1249+
11961250
func TestJwksHeaders(t *testing.T) {
11971251
tests := []struct {
11981252
name string

0 commit comments

Comments
 (0)