v2.0.0 ae3e752

auth_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close