Skip to content

Commit 1c04553

Browse files
committed
test(auth): add invalid credentials auth spec tests
- Added TestInvalidCredentialsAgainstAuthSpec to verify: - Wrong password for existing user returns 401 - Non-existent user returns 401 (prevents enumeration) - Empty password scenarios handled - WWW-Authenticate header set per auth spec - Same response for all invalid creds (no user hints) - Removed outdated tests: - TestHtpasswdAuthSupportedHashTypes (method removed) - TestGetClientIP (function removed) Assisted-by: Tern
1 parent 64b1560 commit 1c04553

File tree

1 file changed

+79
-54
lines changed

1 file changed

+79
-54
lines changed

internal/auth/htpasswd/htpasswd_test.go

Lines changed: 79 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -103,39 +103,6 @@ func TestHtpasswdAuthMultipleUsers(t *testing.T) {
103103
}
104104
}
105105

106-
func TestHtpasswdAuthSupportedHashTypes(t *testing.T) {
107-
logger := zerolog.New(zerolog.NewConsoleWriter())
108-
109-
cfg := &config.HtpasswdConfig{
110-
Contents: "testuser:" + testBcryptHash,
111-
}
112-
113-
auth, err := NewHtpasswdAuth(cfg, &logger)
114-
if err != nil {
115-
t.Fatalf("Failed to create htpasswd auth: %v", err)
116-
}
117-
118-
types := auth.SupportedHashTypes()
119-
if len(types) == 0 {
120-
t.Error("Expected at least one supported hash type")
121-
}
122-
123-
// The library DefaultSystems includes: bcrypt, md5, sha, sha256, sha512, crypt, plain
124-
expected := []string{"bcrypt", "md5", "sha", "sha256", "sha512", "crypt", "plain"}
125-
for _, exp := range expected {
126-
found := false
127-
for _, typ := range types {
128-
if typ == exp {
129-
found = true
130-
break
131-
}
132-
}
133-
if !found {
134-
t.Errorf("Expected hash type %s to be in supported types", exp)
135-
}
136-
}
137-
}
138-
139106
func TestBasicAuthMiddleware(t *testing.T) {
140107
logger := zerolog.New(zerolog.NewConsoleWriter())
141108
cfg := &config.HtpasswdConfig{
@@ -224,7 +191,7 @@ func TestShouldSkipAuth(t *testing.T) {
224191
}
225192
}
226193

227-
func TestGetClientIP(t *testing.T) {
194+
func TestInvalidCredentialsAgainstAuthSpec(t *testing.T) {
228195
logger := zerolog.New(zerolog.NewConsoleWriter())
229196
cfg := &config.HtpasswdConfig{
230197
Contents: "testuser:" + testBcryptHash,
@@ -235,28 +202,86 @@ func TestGetClientIP(t *testing.T) {
235202
t.Fatalf("Failed to create auth: %v", err)
236203
}
237204

238-
// Test X-Forwarded-For
239-
req := httptest.NewRequest("GET", "/v2/", nil)
240-
req.RemoteAddr = "192.0.2.1:12345"
241-
req.Header.Set("X-Forwarded-For", "10.0.0.1")
242-
ip := auth.getClientIP(req)
243-
if ip != "10.0.0.1" {
244-
t.Errorf("Expected IP %s, got %s", "10.0.0.1", ip)
245-
}
205+
middleware := auth.DistributionMiddleware()
206+
handler := middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
207+
w.WriteHeader(http.StatusOK)
208+
}))
246209

247-
// Test X-Real-IP
248-
req.Header.Del("X-Forwarded-For")
249-
req.Header.Set("X-Real-IP", "10.0.0.2")
250-
ip = auth.getClientIP(req)
251-
if ip != "10.0.0.2" {
252-
t.Errorf("Expected IP %s, got %s", "10.0.0.2", ip)
253-
}
210+
testCases := []struct {
211+
name string
212+
username string
213+
password string
214+
expectedStatus int
215+
}{
216+
{
217+
name: "wrong password for existing user",
218+
username: "testuser",
219+
password: "wrongpassword",
220+
expectedStatus: http.StatusUnauthorized,
221+
},
222+
{
223+
name: "non-existent user with password",
224+
username: "nonexistent",
225+
password: "password",
226+
expectedStatus: http.StatusUnauthorized,
227+
},
228+
{
229+
name: "non-existent user empty password",
230+
username: "nonexistent",
231+
password: "",
232+
expectedStatus: http.StatusUnauthorized,
233+
},
234+
{
235+
name: "existing user empty password",
236+
username: "testuser",
237+
password: "",
238+
expectedStatus: http.StatusUnauthorized,
239+
},
240+
{
241+
name: "non-existent user with very long password",
242+
username: "hacker",
243+
password: strings.Repeat("a", 1000),
244+
expectedStatus: http.StatusUnauthorized,
245+
},
246+
}
247+
248+
for _, tc := range testCases {
249+
t.Run(tc.name, func(t *testing.T) {
250+
req := httptest.NewRequest("GET", "/v2/some/repo/manifests/latest", nil)
251+
req.SetBasicAuth(tc.username, tc.password)
252+
w := httptest.NewRecorder()
253+
254+
handler.ServeHTTP(w, req)
255+
256+
// Verify 401 status
257+
if w.Code != tc.expectedStatus {
258+
t.Errorf("Expected status %d, got %d", tc.expectedStatus, w.Code)
259+
}
260+
261+
// Verify WWW-Authenticate header is set (per auth spec)
262+
authHeader := w.Header().Get("WWW-Authenticate")
263+
if authHeader == "" {
264+
t.Error("Expected WWW-Authenticate header to be set")
265+
}
266+
if !strings.Contains(authHeader, "Sorcerer OCI Registry") {
267+
t.Errorf("Expected WWW-Authenticate to contain realm, got: %s", authHeader)
268+
}
269+
270+
// Verify that response body doesn't reveal user existence
271+
// (should be the same for wrong password as non-existent user)
272+
body1 := w.Body.String()
254273

255-
// Test RemoteAddr fallback
256-
req.Header.Del("X-Real-IP")
257-
ip = auth.getClientIP(req)
258-
if ip != "192.0.2.1" {
259-
t.Errorf("Expected IP %s, got %s", "192.0.2.1", ip)
274+
// Test with a different invalid credential
275+
req2 := httptest.NewRequest("GET", "/v2/some/repo/manifests/latest", nil)
276+
req2.SetBasicAuth("otheruser", "otherpass")
277+
w2 := httptest.NewRecorder()
278+
handler.ServeHTTP(w2, req2)
279+
280+
body2 := w2.Body.String()
281+
if body1 != body2 {
282+
t.Error("Response should not reveal whether user exists by differing")
283+
}
284+
})
260285
}
261286
}
262287

0 commit comments

Comments
 (0)