Repository: avenga/couper
Pull request #132 (2021-03-10 13:50:29)

Fix missing access control context in hcl eval context


Patch:
diff --git a/accesscontrol/ac.go b/accesscontrol/ac.go
index 73eae01e..aa316c31 100644
--- a/accesscontrol/ac.go
+++ b/accesscontrol/ac.go
@@ -1,10 +1,12 @@
 package accesscontrol
 
-import "net/http"
+import (
+	"net/http"
 
-var _ AccessControl = ValidateFunc(func(_ *http.Request) error { return nil })
+	"github.com/avenga/couper/eval"
+)
 
-const ContextAccessControlKey = "access_controls"
+var _ AccessControl = ValidateFunc(func(_ *http.Request) error { return nil })
 
 type (
 	Map  map[string]AccessControl
@@ -22,7 +24,14 @@ type ProtectedHandler interface {
 }
 
 func (f ValidateFunc) Validate(req *http.Request) error {
-	return f(req)
+	if err := f(req); err != nil {
+		return err
+	}
+
+	if evalCtx, ok := req.Context().Value(eval.ContextType).(*eval.Context); ok {
+		*req = *req.WithContext(evalCtx.WithClientRequest(req))
+	}
+	return nil
 }
 
 func (m Map) MustExist(name string) {
diff --git a/accesscontrol/jwt.go b/accesscontrol/jwt.go
index 09927507..f3fac37a 100644
--- a/accesscontrol/jwt.go
+++ b/accesscontrol/jwt.go
@@ -13,6 +13,8 @@ import (
 	"time"
 
 	"github.com/dgrijalva/jwt-go/v4"
+
+	"github.com/avenga/couper/config/request"
 )
 
 const (
@@ -34,13 +36,12 @@ var (
 
 type (
 	Algorithm int
-	Claims    map[string]interface{}
 	Source    int
 )
 
 type JWT struct {
 	algorithm      Algorithm
-	claims         Claims
+	claims         map[string]interface{}
 	claimsRequired []string
 	ignoreExp      bool
 	source         Source
@@ -52,7 +53,7 @@ type JWT struct {
 }
 
 // NewJWT parses the key and creates Validation obj which can be referenced in related handlers.
-func NewJWT(algorithm, name string, claims Claims, reqClaims []string, src Source, srcKey string, key []byte) (*JWT, error) {
+func NewJWT(algorithm, name string, claims map[string]interface{}, reqClaims []string, src Source, srcKey string, key []byte) (*JWT, error) {
 	if len(key) == 0 {
 		return nil, ErrorMissingKey
 	}
@@ -152,12 +153,13 @@ func (j *JWT) Validate(req *http.Request) error {
 	}
 
 	ctx := req.Context()
-	acMap, ok := ctx.Value(ContextAccessControlKey).(map[string]interface{})
+	acMap, ok := ctx.Value(request.AccessControls).(map[string]interface{})
 	if !ok {
 		acMap = make(map[string]interface{})
 	}
 	acMap[j.name] = tokenClaims
-	ctx = context.WithValue(ctx, ContextAccessControlKey, acMap)
+
+	ctx = context.WithValue(ctx, request.AccessControls, acMap)
 	*req = *req.WithContext(ctx)
 
 	return nil
@@ -174,7 +176,7 @@ func (j *JWT) getValidationKey(_ *jwt.Token) (interface{}, error) {
 	}
 }
 
-func (j *JWT) validateClaims(token *jwt.Token) (Claims, error) {
+func (j *JWT) validateClaims(token *jwt.Token) (map[string]interface{}, error) {
 	var tokenClaims jwt.MapClaims
 	if tc, ok := token.Claims.(jwt.MapClaims); ok {
 		tokenClaims = tc
@@ -205,7 +207,7 @@ func (j *JWT) validateClaims(token *jwt.Token) (Claims, error) {
 			return nil, errors.New("unexpected value for claim '" + k + "'")
 		}
 	}
-	return Claims(tokenClaims), nil
+	return tokenClaims, nil
 }
 
 func getBearer(val string) (string, error) {
@@ -216,7 +218,7 @@ func getBearer(val string) (string, error) {
 	return "", ErrorBearerRequired
 }
 
-func newParser(algo Algorithm, claims Claims) (*jwt.Parser, error) {
+func newParser(algo Algorithm, claims map[string]interface{}) (*jwt.Parser, error) {
 	options := []jwt.ParserOption{
 		jwt.WithValidMethods([]string{algo.String()}),
 		jwt.WithLeeway(time.Second),
diff --git a/config/request/context_key.go b/config/request/context_key.go
index 93191ebb..32261ad4 100644
--- a/config/request/context_key.go
+++ b/config/request/context_key.go
@@ -4,6 +4,7 @@ type ContextKey uint8
 
 const (
 	UID ContextKey = iota
+	AccessControls
 	BackendName
 	Endpoint
 	EndpointKind
diff --git a/config/runtime/server.go b/config/runtime/server.go
index a40e4293..84ab8626 100644
--- a/config/runtime/server.go
+++ b/config/runtime/server.go
@@ -378,7 +378,7 @@ func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext) (ac.
 				return nil, err
 			}
 
-			accessControls[name] = basicAuth
+			accessControls[name] = ac.ValidateFunc(basicAuth.Validate)
 		}
 
 		for _, jwt := range conf.Definitions.JWT {
@@ -411,7 +411,7 @@ func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext) (ac.
 				key = []byte(jwt.Key)
 			}
 
-			var claims ac.Claims
+			var claims map[string]interface{}
 			if jwt.Claims != nil {
 				c, diags := seetie.ExpToMap(confCtx, jwt.Claims)
 				if diags.HasErrors() {
@@ -424,7 +424,7 @@ func configureAccessControls(conf *config.Couper, confCtx *hcl.EvalContext) (ac.
 				return nil, fmt.Errorf("loading jwt %q definition failed: %s", name, err)
 			}
 
-			accessControls[name] = j
+			accessControls[name] = ac.ValidateFunc(j.Validate)
 		}
 	}
 
diff --git a/eval/context.go b/eval/context.go
index 5d9f5ff5..629ff1e7 100644
--- a/eval/context.go
+++ b/eval/context.go
@@ -19,7 +19,6 @@ import (
 	"github.com/zclconf/go-cty/cty/function"
 	"github.com/zclconf/go-cty/cty/function/stdlib"
 
-	ac "github.com/avenga/couper/accesscontrol"
 	"github.com/avenga/couper/config/request"
 	"github.com/avenga/couper/eval/lib"
 	"github.com/avenga/couper/internal/seetie"
@@ -248,10 +247,10 @@ func cloneContext(ctx *hcl.EvalContext) *hcl.EvalContext {
 }
 
 func newVariable(ctx context.Context, cookies []*http.Cookie, headers http.Header) ContextMap {
-	jwtClaims, _ := ctx.Value(ac.ContextAccessControlKey).(map[string]interface{})
+	jwtClaims, _ := ctx.Value(request.AccessControls).(map[string]interface{})
 	ctxAcMap := make(map[string]cty.Value)
 	for name, data := range jwtClaims {
-		dataMap, ok := data.(ac.Claims)
+		dataMap, ok := data.(map[string]interface{})
 		if !ok {
 			continue
 		}


Test patch:
diff --git a/accesscontrol/jwt_test.go b/accesscontrol/jwt_test.go
index 558b2709..81ed30e8 100644
--- a/accesscontrol/jwt_test.go
+++ b/accesscontrol/jwt_test.go
@@ -13,12 +13,13 @@ import (
 	"github.com/dgrijalva/jwt-go/v4"
 
 	ac "github.com/avenga/couper/accesscontrol"
+	"github.com/avenga/couper/config/request"
 )
 
 func TestJWT_Validate(t *testing.T) {
 	type fields struct {
 		algorithm      ac.Algorithm
-		claims         ac.Claims
+		claims         map[string]interface{}
 		claimsRequired []string
 		source         ac.Source
 		sourceKey      string
@@ -91,7 +92,7 @@ func TestJWT_Validate(t *testing.T) {
 			}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "token", token), false},
 			{"src: header /w valid bearer & claims", fields{
 				algorithm: algo,
-				claims: ac.Claims{
+				claims: map[string]interface{}{
 					"aud":     "peter",
 					"test123": "value123",
 				},
@@ -102,7 +103,7 @@ func TestJWT_Validate(t *testing.T) {
 			}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), false},
 			{"src: header /w valid bearer & w/o claims", fields{
 				algorithm: algo,
-				claims: ac.Claims{
+				claims: map[string]interface{}{
 					"aud":  "peter",
 					"cptn": "hook",
 				},
@@ -112,7 +113,7 @@ func TestJWT_Validate(t *testing.T) {
 			}, setCookieAndHeader(httptest.NewRequest(http.MethodGet, "/", nil), "Authorization", "BeAreR "+token), true},
 			{"src: header /w valid bearer & w/o required claims", fields{
 				algorithm: algo,
-				claims: ac.Claims{
+				claims: map[string]interface{}{
 					"aud": "peter",
 				},
 				claimsRequired: []string{"exp"},
@@ -130,11 +131,11 @@ func TestJWT_Validate(t *testing.T) {
 				}
 
 				if !tt.wantErr && tt.fields.claims != nil {
-					acMap := tt.req.Context().Value(ac.ContextAccessControlKey).(map[string]interface{})
+					acMap := tt.req.Context().Value(request.AccessControls).(map[string]interface{})
 					if claims, ok := acMap["test_ac"]; !ok {
 						t.Errorf("Expected a configured access control name within request context")
 					} else {
-						claimsMap := claims.(ac.Claims)
+						claimsMap := claims.(map[string]interface{})
 						for k, v := range tt.fields.claims {
 							if claimsMap[k] != v {
 								t.Errorf("Claim does not match: %q want: %v, got: %v", k, v, claimsMap[k])
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 13de84f7..2a720849 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -1156,6 +1156,52 @@ func TestConfigBodyContentAccessControl(t *testing.T) {
 	}
 }
 
+func TestJWTAccessControl(t *testing.T) {
+	client := newClient()
+
+	shutdown, _ := newCouper("testdata/integration/config/03_couper.hcl", test.New(t))
+	defer shutdown()
+
+	type testCase struct {
+		name   string
+		path   string
+		header http.Header
+		status int
+	}
+
+	for _, tc := range []testCase{
+		{"no token", "/jwt", http.Header{}, http.StatusUnauthorized},
+		{"expired token", "/jwt", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjEyMzQ1Njc4OX0.wLWj9XgBZAPoDYPXsmDrEBzR6BUWfwPqQNlR_F0naZA"}}, http.StatusForbidden},
+		{"valid token", "/jwt", http.Header{"Authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.Qf0lkeZKZ3NJrYm3VdgiQiQ6QTrjCvISshD_q9F8GAM"}}, http.StatusOK},
+	} {
+		t.Run(tc.path[1:], func(subT *testing.T) {
+			helper := test.New(subT)
+			req, err := http.NewRequest(http.MethodGet, "http://back.end:8080"+tc.path, nil)
+			helper.Must(err)
+
+			if val := tc.header.Get("Authorization"); val != "" {
+				req.Header.Set("Authorization", val)
+			}
+
+			res, err := client.Do(req)
+			helper.Must(err)
+
+			if res.StatusCode != tc.status {
+				t.Errorf("%q: expected Status %d, got: %d", tc.name, tc.status, res.StatusCode)
+				return
+			}
+
+			if res.StatusCode != http.StatusOK {
+				return
+			}
+			if sub := res.Header.Get("X-Jwt-Sub"); sub != "1234567890" {
+				t.Errorf("%q: unexpected sub: %q", tc.name, sub)
+				return
+			}
+		})
+	}
+}
+
 func TestWrapperHiJack_WebsocketUpgrade(t *testing.T) {
 	t.Skip("TODO fix hijack and endpoint handling for ws")
 	helper := test.New(t)
diff --git a/server/testdata/integration/config/03_couper.hcl b/server/testdata/integration/config/03_couper.hcl
index ff14c660..cf93cd6c 100644
--- a/server/testdata/integration/config/03_couper.hcl
+++ b/server/testdata/integration/config/03_couper.hcl
@@ -61,6 +61,16 @@ server "acs" {
       }
     }
   }
+
+  endpoint "/jwt" {
+    disable_access_control = ["ba1"]
+    access_control = ["JWTToken"]
+    response {
+      headers = {
+        x-jwt-sub = req.ctx.JWTToken.sub
+      }
+    }
+  }
 }
 
 definitions {
@@ -76,6 +86,11 @@ definitions {
   basic_auth "ba4" {
     password = "asdf"
   }
+  jwt "JWTToken" {
+    header = "Authorization"
+    signature_algorithm = "HS256"
+    key = "y0urS3cretT08eU5edF0rC0uPerInThe3xamp1e"
+  }
 
   backend "test" {
     origin = env.COUPER_TEST_BACKEND_ADDR