Repository: avenga/couper
Pull request #115 (2021-02-08 13:25:46)

basic auth access control with empty password


Patch:
diff --git a/accesscontrol/basic_auth.go b/accesscontrol/basic_auth.go
index e85e9cdd..68cda9ee 100644
--- a/accesscontrol/basic_auth.go
+++ b/accesscontrol/basic_auth.go
@@ -146,6 +146,10 @@ func (ba *BasicAuth) Validate(req *http.Request) error {
 		return ErrorBasicAuthNotConfigured
 	}
 
+	if ba.pass == "" {
+		return &BasicAuthError{error: ErrorBasicAuthUnauthorized, Realm: ba.realm}
+	}
+
 	user, pass, ok := req.BasicAuth()
 	if !ok {
 		return &BasicAuthError{error: ErrorBasicAuthMissingCredentials, Realm: ba.realm}


Test patch:
diff --git a/accesscontrol/basic_auth_test.go b/accesscontrol/basic_auth_test.go
index 5a9d50c5..5daa6a9a 100644
--- a/accesscontrol/basic_auth_test.go
+++ b/accesscontrol/basic_auth_test.go
@@ -18,6 +18,9 @@ func Test_NewBasicAuth(t *testing.T) {
 
 	for _, tc := range []testCase{
 		{"name", "user", "pass", "", "Basic", "", false},
+		{"name", "user", "", "", "Basic", "", false},
+		{"name", "", "pass", "", "Basic", "", false},
+		{"name", "", "", "", "Basic", "", false},
 		{"name", "user", "pass", "testdata/htpasswd", "Basic", "", false},
 		{"name", "john", "pass", "testdata/htpasswd", "Basic", "", false},
 		{"name", "user", "pass", "file", "Basic", "open file: no such file or directory", true},
@@ -83,3 +86,111 @@ func Test_Validate(t *testing.T) {
 		})
 	}
 }
+
+func Test_ValidateEmptyUser(t *testing.T) {
+	var ba *ac.BasicAuth
+	req := &http.Request{Header: make(http.Header)}
+
+	if err := ba.Validate(req); err != ac.ErrorBasicAuthNotConfigured {
+		t.Errorf("Expected NotConfigured error, got: %v", err)
+	}
+
+	ba, err := ac.NewBasicAuth("name", "", "pass", "", "Basic")
+	if err != nil || ba == nil {
+		t.Fatal("Expected a basic auth object")
+	}
+
+	var authError *ac.BasicAuthError
+
+	type testCase struct {
+		headerValue string
+		expErr      error
+	}
+
+	for _, testcase := range []testCase{
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte("user:pass")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte("user:")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte(":pass")), nil},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte(":")), authError},
+	} {
+		t.Run(testcase.headerValue, func(t *testing.T) {
+			req.Header.Set("Authorization", testcase.headerValue)
+			err := ba.Validate(req)
+			if testcase.expErr != nil && !errors.As(err, &testcase.expErr) {
+				t.Errorf("Expected Unauthorized error, got: %v", err)
+			}
+		})
+	}
+}
+
+func Test_ValidateEmptyPassword(t *testing.T) {
+	var ba *ac.BasicAuth
+	req := &http.Request{Header: make(http.Header)}
+
+	if err := ba.Validate(req); err != ac.ErrorBasicAuthNotConfigured {
+		t.Errorf("Expected NotConfigured error, got: %v", err)
+	}
+
+	ba, err := ac.NewBasicAuth("name", "user", "", "", "Basic")
+	if err != nil || ba == nil {
+		t.Fatal("Expected a basic auth object")
+	}
+
+	var authError *ac.BasicAuthError
+
+	type testCase struct {
+		headerValue string
+		expErr      error
+	}
+
+	for _, testcase := range []testCase{
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte("user:pass")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte("user:")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte(":pass")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte(":")), authError},
+	} {
+		t.Run(testcase.headerValue, func(t *testing.T) {
+			req.Header.Set("Authorization", testcase.headerValue)
+			err := ba.Validate(req)
+			if testcase.expErr != nil && !errors.As(err, &testcase.expErr) {
+				t.Errorf("Expected Unauthorized error, got: %v", err)
+			}
+		})
+	}
+}
+
+func Test_ValidateEmptyUserPassword(t *testing.T) {
+	var ba *ac.BasicAuth
+	req := &http.Request{Header: make(http.Header)}
+
+	if err := ba.Validate(req); err != ac.ErrorBasicAuthNotConfigured {
+		t.Errorf("Expected NotConfigured error, got: %v", err)
+	}
+
+	ba, err := ac.NewBasicAuth("name", "", "", "", "Basic")
+	if err != nil || ba == nil {
+		t.Fatal("Expected a basic auth object")
+	}
+
+	var authError *ac.BasicAuthError
+
+	type testCase struct {
+		headerValue string
+		expErr      error
+	}
+
+	for _, testcase := range []testCase{
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte("user:pass")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte("user:")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte(":pass")), authError},
+		{"Basic " + b64.StdEncoding.EncodeToString([]byte(":")), authError},
+	} {
+		t.Run(testcase.headerValue, func(t *testing.T) {
+			req.Header.Set("Authorization", testcase.headerValue)
+			err := ba.Validate(req)
+			if testcase.expErr != nil && !errors.As(err, &testcase.expErr) {
+				t.Errorf("Expected Unauthorized error, got: %v", err)
+			}
+		})
+	}
+}
diff --git a/server/http_integration_test.go b/server/http_integration_test.go
index 845f3520..24e5bffb 100644
--- a/server/http_integration_test.go
+++ b/server/http_integration_test.go
@@ -1122,13 +1122,13 @@ func TestConfigBodyContentAccessControl(t *testing.T) {
 	for _, tc := range []testCase{
 		{"/v1", http.Header{"Auth": []string{"ba1"}}, http.StatusOK},
 		// TODO: Can a disabled auth being enabled again?
-		//{"/v1", http.Header{"Authorization": []string{"Basic Og=="}, "Auth": []string{"ba1"}}, http.StatusOK},
+		//{"/v1", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1"}}, http.StatusOK},
 		//{"/v1", http.Header{"Auth": []string{}}, http.StatusUnauthorized},
-		{"/v2", http.Header{"Authorization": []string{"Basic Og=="}, "Auth": []string{"ba1", "ba2"}}, http.StatusOK}, // minimum ':'
+		{"/v2", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba2"}}, http.StatusOK}, // minimum ':'
 		{"/v2", http.Header{}, http.StatusUnauthorized},
 		{"/v3", http.Header{}, http.StatusOK},
 		{"/status", http.Header{}, http.StatusOK},
-		{"/superadmin", http.Header{"Authorization": []string{"Basic Og=="}, "Auth": []string{"ba1", "ba4"}}, http.StatusOK},
+		{"/superadmin", http.Header{"Authorization": []string{"Basic OmFzZGY="}, "Auth": []string{"ba1", "ba4"}}, http.StatusOK},
 		{"/superadmin", http.Header{}, http.StatusUnauthorized},
 	} {
 		t.Run(tc.path[1:], func(subT *testing.T) {
diff --git a/server/testdata/integration/config/03_couper.hcl b/server/testdata/integration/config/03_couper.hcl
index a53f2502..a5d54205 100644
--- a/server/testdata/integration/config/03_couper.hcl
+++ b/server/testdata/integration/config/03_couper.hcl
@@ -51,10 +51,18 @@ server "acs" {
 }
 
 definitions {
-  basic_auth "ba1" {}
-  basic_auth "ba2" {}
-  basic_auth "ba3" {}
-  basic_auth "ba4" {}
+  basic_auth "ba1" {
+    password = "asdf"
+  }
+  basic_auth "ba2" {
+    password = "asdf"
+  }
+  basic_auth "ba3" {
+    password = "asdf"
+  }
+  basic_auth "ba4" {
+    password = "asdf"
+  }
 
   backend "test" {
     origin = env.COUPER_TEST_BACKEND_ADDR