Skip to content

Commit

Permalink
feat: add source config of the JWT bearer/header/cookie/query
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
simonpahl committed Jun 29, 2024
1 parent 12a6076 commit 339b7af
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 21 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ JwtHeaders | Map used to inject JWT payload fields as HTTP request headers.
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.
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.
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.
JwtCookieKey | Name of the cookie to extract JWT if not found in `Authorization` header.
JwtQueryKey | Name of the query parameter to extract JWT if not found in `Authorization` header or in the specified cookie.
JwtCookieKey | (Deprecated, use JwtSources)Name of the cookie to extract JWT if not found in `Authorization` header.
JwtQueryKey | (Deprecated, use JwtSources) Name of the query parameter to extract JWT if not found in `Authorization` header or in the specified cookie.
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}]


### Example configuration

Expand Down Expand Up @@ -98,7 +100,11 @@ spec:
OpaResponseHeaders:
X-Allowed: allow
OpaHttpStatusField: allow_status_code
JwtCookieKey: jwt
JwtSources:
- type: bearer
key: Authorization
- type: cookie
key: jwt
---
apiVersion: networking.k8s.io/v1
kind: Ingress
Expand Down
77 changes: 59 additions & 18 deletions jwt.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ type Config struct {
JwksHeaders map[string]string
OpaResponseHeaders map[string]string
OpaHttpStatusField string
JwtCookieKey string
JwtQueryKey string
JwtCookieKey string // Deprecated: use JwtSources instead
JwtQueryKey string // Deprecated: use JwtSources instead
JwtSources []map[string]string
}

// CreateConfig creates a new OPA Config
Expand Down Expand Up @@ -77,8 +78,9 @@ type JwtPlugin struct {
jwksHeaders map[string]string
opaResponseHeaders map[string]string
opaHttpStatusField string
jwtCookieKey string
jwtQueryKey string
jwtCookieKey string // Deprecated: use jwtSources instead
jwtQueryKey string // Deprecated: use jwtSources instead
jwtSources []map[string]string

name string
keysLock sync.RWMutex
Expand Down Expand Up @@ -193,8 +195,19 @@ func New(ctx context.Context, next http.Handler, config *Config, pluginName stri
opaHttpStatusField: config.OpaHttpStatusField,
jwtCookieKey: config.JwtCookieKey,
jwtQueryKey: config.JwtQueryKey,
jwtSources: config.JwtSources,
name: pluginName,
}
// use default order if jwtSourceOrder is set
if len(jwtPlugin.jwtSources) == 0 {
jwtPlugin.jwtSources = []map[string]string{{"type": "bearer", "key": "Authorization"}}
if jwtPlugin.jwtCookieKey != "" {
jwtPlugin.jwtSources = append(jwtPlugin.jwtSources, map[string]string{"type": "cookie", "key": jwtPlugin.jwtCookieKey})
}
if jwtPlugin.jwtQueryKey != "" {
jwtPlugin.jwtSources = append(jwtPlugin.jwtSources, map[string]string{"type": "query", "key": jwtPlugin.jwtQueryKey})
}
}
if len(config.Keys) > 0 {
if err := jwtPlugin.ParseKeys(config.Keys); err != nil {
return nil, err
Expand Down Expand Up @@ -486,13 +499,33 @@ func (jwtPlugin *JwtPlugin) CheckToken(request *http.Request, rw http.ResponseWr
}

func (jwtPlugin *JwtPlugin) ExtractToken(request *http.Request) (*JWT, error) {
// first check if the token is present in header and is valid
jwtTokenStr, err := jwtPlugin.extractTokenFromHeader(request)
if err != nil && jwtPlugin.jwtCookieKey != "" {
jwtTokenStr, err = jwtPlugin.extractTokenFromCookie(request)
}
if err != nil && jwtPlugin.jwtQueryKey != "" {
jwtTokenStr, err = jwtPlugin.extractTokenFromQuery(request)
// extract from header, cookie, or query with given priority
var jwtTokenStr string
var err error
for _, sourceconfig := range jwtPlugin.jwtSources {
sourcetype, oktype := sourceconfig["type"]
if !oktype || (sourcetype != "bearer" && sourcetype != "header" && sourcetype != "cookie" && sourcetype != "query") {
jwtTokenStr, err = "", fmt.Errorf("source type unknown")
continue
}
sourcekey, okkey := sourceconfig["key"]
if !okkey || sourcekey == "" {
jwtTokenStr, err = "", fmt.Errorf("source key not found or empty")
continue
}
switch sourcetype {
case "bearer":
jwtTokenStr, err = jwtPlugin.extractTokenFromBearer(request, sourcekey)
case "header":
jwtTokenStr, err = jwtPlugin.extractTokenFromHeader(request, sourcekey)
case "cookie":
jwtTokenStr, err = jwtPlugin.extractTokenFromCookie(request, sourcekey)
case "query":
jwtTokenStr, err = jwtPlugin.extractTokenFromQuery(request, sourcekey)
}
if err == nil && jwtTokenStr != "" {
break
}
}
if err != nil {
return nil, err
Expand Down Expand Up @@ -535,32 +568,40 @@ func (jwtPlugin *JwtPlugin) ExtractToken(request *http.Request) (*JWT, error) {
return &jwtToken, nil
}

func (jwtPlugin *JwtPlugin) extractTokenFromHeader(request *http.Request) (string, error) {
authHeader, ok := request.Header["Authorization"]
func (jwtPlugin *JwtPlugin) extractTokenFromHeader(request *http.Request, key string) (string, error) {
authHeader, ok := request.Header[key]
if !ok {
return "", fmt.Errorf("authorization header missing")
}
auth := authHeader[0]
return auth, nil
}

func (jwtPlugin *JwtPlugin) extractTokenFromBearer(request *http.Request, key string) (string, error) {
auth, err := jwtPlugin.extractTokenFromHeader(request, key)
if err != nil {
return auth, err
}
if !strings.HasPrefix(strings.ToLower(auth), "bearer ") {
return "", fmt.Errorf("authorization type not Bearer")
}
return auth[7:], nil
}

func (jwtPlugin *JwtPlugin) extractTokenFromCookie(request *http.Request) (string, error) {
cookie, err := request.Cookie(jwtPlugin.jwtCookieKey)
func (jwtPlugin *JwtPlugin) extractTokenFromCookie(request *http.Request, key string) (string, error) {
cookie, err := request.Cookie(key)
if err != nil {
return "", err
}
return cookie.Value, nil
}

func (jwtPlugin *JwtPlugin) extractTokenFromQuery(request *http.Request) (string, error) {
func (jwtPlugin *JwtPlugin) extractTokenFromQuery(request *http.Request, key string) (string, error) {
query := request.URL.Query()
if !query.Has(jwtPlugin.jwtQueryKey) {
if !query.Has(key) {
return "", fmt.Errorf("query parameter missing")
}
parameter := query.Get(jwtPlugin.jwtQueryKey)
parameter := query.Get(key)
return parameter, nil
}

Expand Down
54 changes: 54 additions & 0 deletions jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,60 @@ func TestTokenFromQueryConfiguredButNotInURL(t *testing.T) {
}
}

func TestTokenFromHeaderConfigured(t *testing.T) {
cfg := *CreateConfig()
cfg.JwtSources = []map[string]string{{"type": "header", "key": "X-JWT"}}
ctx := context.Background()
nextCalled := false
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true })

jwt, err := New(ctx, next, &cfg, "test-traefik-jwt-plugin")
if err != nil {
t.Fatal(err)
}

recorder := httptest.NewRecorder()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
}
req.Header["X-JWT"] = []string{"eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TnHVsM5_N0SKi_HCwlz3ys1cDktu10g_sKkjqzVe5k09z-bmByflWPFWjAbwgRCKAc77kF8BjDNv0gisAPurBxgxNGxioDFehhcb0IS0YeCAWpzRfBMT6gQZ1gZeNM2Dg_yf4shPhF4rcUCGqnFFzIDSU9Rv2NNMK5DPO4512uTxAQUMHpi5PGTki-zykqTB10Ju1L4jRhmJwJDtGcfdHPlEKKUrFPfYl3RPZLOfdyAqSJ8Gi0R3ymDffmXHz08AJUAY_Kapk8laggIYcvFJhYGJBWZpcy7NWMiOIjEI3bogki4o7z0-Z1xMZdZ9rqypQ1MB44F8VZS2KkPfEmhSog"}

jwt.ServeHTTP(recorder, req)

if nextCalled == false {
t.Fatal("next.ServeHTTP was not called")
}
}

func TestTokenSourceOrder(t *testing.T) {
cfg := *CreateConfig()
cfg.JwtSources = []map[string]string{{"type": "header", "key": "X-JWT"}, {"type": "cookie", "key": "jwt"}}
ctx := context.Background()
nextCalled := false
next := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { nextCalled = true })

jwt, err := New(ctx, next, &cfg, "test-traefik-jwt-plugin")
if err != nil {
t.Fatal(err)
}

recorder := httptest.NewRecorder()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://localhost", nil)
if err != nil {
t.Fatal(err)
}
req.AddCookie(&http.Cookie{Name: "jwt", Value: "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.TnHVsM5_N0SKi_HCwlz3ys1cDktu10g_sKkjqzVe5k09z-bmByflWPFWjAbwgRCKAc77kF8BjDNv0gisAPurBxgxNGxioDFehhcb0IS0YeCAWpzRfBMT6gQZ1gZeNM2Dg_yf4shPhF4rcUCGqnFFzIDSU9Rv2NNMK5DPO4512uTxAQUMHpi5PGTki-zykqTB10Ju1L4jRhmJwJDtGcfdHPlEKKUrFPfYl3RPZLOfdyAqSJ8Gi0R3ymDffmXHz08AJUAY_Kapk8laggIYcvFJhYGJBWZpcy7NWMiOIjEI3bogki4o7z0-Z1xMZdZ9rqypQ1MB44F8VZS2KkPfEmhSog"})

jwt.ServeHTTP(recorder, req)

if nextCalled == false {
t.Fatal("next.ServeHTTP was not called")
}
}

func TestJwksHeaders(t *testing.T) {
tests := []struct {
name string
Expand Down

0 comments on commit 339b7af

Please sign in to comment.