1package middleware
2
3import (
4 "encoding/base64"
5 "net/http"
6 "net/http/httptest"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10)
11
12func TestRegistryAuth(t *testing.T) {
13 username := "testuser"
14 password := "testpass"
15
16 // Create test handler that the middleware wraps
17 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 w.WriteHeader(http.StatusOK)
19 w.Write([]byte("authenticated"))
20 })
21
22 middleware := RegistryAuth(username, password)
23 handler := middleware(testHandler)
24
25 tests := []struct {
26 name string
27 setupAuth func(*http.Request)
28 expectedStatus int
29 expectedBody string
30 checkHeaders bool
31 }{
32 {
33 name: "valid credentials",
34 setupAuth: func(r *http.Request) {
35 auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
36 r.Header.Set("Authorization", "Basic "+auth)
37 },
38 expectedStatus: http.StatusOK,
39 expectedBody: "authenticated",
40 },
41 {
42 name: "invalid username",
43 setupAuth: func(r *http.Request) {
44 auth := base64.StdEncoding.EncodeToString([]byte("wronguser:" + password))
45 r.Header.Set("Authorization", "Basic "+auth)
46 },
47 expectedStatus: http.StatusUnauthorized,
48 expectedBody: "Unauthorized\n",
49 checkHeaders: true,
50 },
51 {
52 name: "invalid password",
53 setupAuth: func(r *http.Request) {
54 auth := base64.StdEncoding.EncodeToString([]byte(username + ":wrongpass"))
55 r.Header.Set("Authorization", "Basic "+auth)
56 },
57 expectedStatus: http.StatusUnauthorized,
58 expectedBody: "Unauthorized\n",
59 checkHeaders: true,
60 },
61 {
62 name: "no authorization header",
63 setupAuth: func(r *http.Request) {
64 // No auth header
65 },
66 expectedStatus: http.StatusUnauthorized,
67 expectedBody: "Unauthorized\n",
68 checkHeaders: true,
69 },
70 {
71 name: "malformed authorization header",
72 setupAuth: func(r *http.Request) {
73 r.Header.Set("Authorization", "Invalid auth")
74 },
75 expectedStatus: http.StatusUnauthorized,
76 expectedBody: "Unauthorized\n",
77 checkHeaders: true,
78 },
79 {
80 name: "empty credentials",
81 setupAuth: func(r *http.Request) {
82 auth := base64.StdEncoding.EncodeToString([]byte(":"))
83 r.Header.Set("Authorization", "Basic "+auth)
84 },
85 expectedStatus: http.StatusUnauthorized,
86 expectedBody: "Unauthorized\n",
87 checkHeaders: true,
88 },
89 }
90
91 for _, tt := range tests {
92 t.Run(tt.name, func(t *testing.T) {
93 req := httptest.NewRequest("GET", "/v2/test", nil)
94 tt.setupAuth(req)
95
96 rr := httptest.NewRecorder()
97 handler.ServeHTTP(rr, req)
98
99 assert.Equal(t, tt.expectedStatus, rr.Code)
100 assert.Equal(t, tt.expectedBody, rr.Body.String())
101
102 if tt.checkHeaders {
103 assert.Equal(t, `Basic realm="Gordon Registry"`, rr.Header().Get("WWW-Authenticate"))
104 assert.Equal(t, "registry/2.0", rr.Header().Get("Docker-Distribution-API-Version"))
105 }
106 })
107 }
108}
109
110func TestIsAuthenticated(t *testing.T) {
111 expectedUsername := "testuser"
112 expectedPassword := "testpass"
113
114 tests := []struct {
115 name string
116 setupRequest func(*http.Request)
117 expectedResult bool
118 }{
119 {
120 name: "valid basic auth",
121 setupRequest: func(r *http.Request) {
122 auth := base64.StdEncoding.EncodeToString([]byte(expectedUsername + ":" + expectedPassword))
123 r.Header.Set("Authorization", "Basic "+auth)
124 },
125 expectedResult: true,
126 },
127 {
128 name: "wrong username",
129 setupRequest: func(r *http.Request) {
130 auth := base64.StdEncoding.EncodeToString([]byte("wronguser:" + expectedPassword))
131 r.Header.Set("Authorization", "Basic "+auth)
132 },
133 expectedResult: false,
134 },
135 {
136 name: "wrong password",
137 setupRequest: func(r *http.Request) {
138 auth := base64.StdEncoding.EncodeToString([]byte(expectedUsername + ":wrongpass"))
139 r.Header.Set("Authorization", "Basic "+auth)
140 },
141 expectedResult: false,
142 },
143 {
144 name: "no authorization header",
145 setupRequest: func(r *http.Request) {
146 // No auth header
147 },
148 expectedResult: false,
149 },
150 {
151 name: "empty authorization header",
152 setupRequest: func(r *http.Request) {
153 r.Header.Set("Authorization", "")
154 },
155 expectedResult: false,
156 },
157 {
158 name: "bearer token instead of basic",
159 setupRequest: func(r *http.Request) {
160 r.Header.Set("Authorization", "Bearer token123")
161 },
162 expectedResult: false,
163 },
164 {
165 name: "malformed basic auth",
166 setupRequest: func(r *http.Request) {
167 r.Header.Set("Authorization", "Basic invalidbase64")
168 },
169 expectedResult: false,
170 },
171 {
172 name: "basic auth without colon",
173 setupRequest: func(r *http.Request) {
174 auth := base64.StdEncoding.EncodeToString([]byte("usernamepassword"))
175 r.Header.Set("Authorization", "Basic "+auth)
176 },
177 expectedResult: false,
178 },
179 {
180 name: "case sensitive username",
181 setupRequest: func(r *http.Request) {
182 auth := base64.StdEncoding.EncodeToString([]byte("TESTUSER:" + expectedPassword))
183 r.Header.Set("Authorization", "Basic "+auth)
184 },
185 expectedResult: false,
186 },
187 {
188 name: "case sensitive password",
189 setupRequest: func(r *http.Request) {
190 auth := base64.StdEncoding.EncodeToString([]byte(expectedUsername + ":TESTPASS"))
191 r.Header.Set("Authorization", "Basic "+auth)
192 },
193 expectedResult: false,
194 },
195 {
196 name: "unicode characters in credentials",
197 setupRequest: func(r *http.Request) {
198 auth := base64.StdEncoding.EncodeToString([]byte("tëstüser:tëstpäss"))
199 r.Header.Set("Authorization", "Basic "+auth)
200 },
201 expectedResult: false,
202 },
203 }
204
205 for _, tt := range tests {
206 t.Run(tt.name, func(t *testing.T) {
207 req := httptest.NewRequest("GET", "/test", nil)
208 tt.setupRequest(req)
209
210 result := isAuthenticated(req, expectedUsername, expectedPassword)
211 assert.Equal(t, tt.expectedResult, result)
212 })
213 }
214}
215
216func TestRegistryAuth_DifferentMethods(t *testing.T) {
217 username := "user"
218 password := "pass"
219
220 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
221 w.WriteHeader(http.StatusOK)
222 })
223
224 middleware := RegistryAuth(username, password)
225 handler := middleware(testHandler)
226
227 methods := []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "PATCH"}
228
229 for _, method := range methods {
230 t.Run("method_"+method, func(t *testing.T) {
231 req := httptest.NewRequest(method, "/v2/test", nil)
232 auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
233 req.Header.Set("Authorization", "Basic "+auth)
234
235 rr := httptest.NewRecorder()
236 handler.ServeHTTP(rr, req)
237
238 assert.Equal(t, http.StatusOK, rr.Code)
239 })
240 }
241}
242
243func TestRegistryAuth_EmptyCredentials(t *testing.T) {
244 // Test with empty username and password
245 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
246 w.WriteHeader(http.StatusOK)
247 })
248
249 middleware := RegistryAuth("", "")
250 handler := middleware(testHandler)
251
252 // Test with empty auth
253 req := httptest.NewRequest("GET", "/v2/test", nil)
254 auth := base64.StdEncoding.EncodeToString([]byte(":"))
255 req.Header.Set("Authorization", "Basic "+auth)
256
257 rr := httptest.NewRecorder()
258 handler.ServeHTTP(rr, req)
259
260 assert.Equal(t, http.StatusOK, rr.Code)
261}
262
263func TestRegistryAuth_TimingAttackResistance(t *testing.T) {
264 // This test ensures that authentication uses constant-time comparison
265 // to prevent timing attacks
266 username := "validuser"
267 password := "validpassword"
268
269 testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
270 w.WriteHeader(http.StatusOK)
271 })
272
273 middleware := RegistryAuth(username, password)
274 handler := middleware(testHandler)
275
276 // Test with various invalid credentials to ensure consistent timing
277 invalidCredentials := []struct {
278 user string
279 pass string
280 }{
281 {"a", "b"}, // Very short
282 {"verylongusernamethatdoesnotmatch", "short"}, // Long username
283 {"short", "verylongpasswordthatdoesnotmatch"}, // Long password
284 {username, "wrongpass"}, // Correct user, wrong pass
285 {"wronguser", password}, // Wrong user, correct pass
286 }
287
288 for _, creds := range invalidCredentials {
289 t.Run("timing_test_"+creds.user, func(t *testing.T) {
290 req := httptest.NewRequest("GET", "/v2/test", nil)
291 auth := base64.StdEncoding.EncodeToString([]byte(creds.user + ":" + creds.pass))
292 req.Header.Set("Authorization", "Basic "+auth)
293
294 rr := httptest.NewRecorder()
295 handler.ServeHTTP(rr, req)
296
297 // All should return unauthorized
298 assert.Equal(t, http.StatusUnauthorized, rr.Code)
299 })
300 }
301}