15
package caddyauth
16
17
import (
18
+ "bufio"
19
+ "bytes"
20
"context"
21
"errors"
22
+ "io"
23
+ "net"
24
"net/http"
25
"net/http/httptest"
26
"testing"
199
func (a staticAuthenticator) Authenticate(http.ResponseWriter, *http.Request) (User, bool, error) {
200
return a.user, a.authed, a.err
201
}
202
+
203
+// writingAuthenticator writes to the response during Authenticate (as a real
204
+// provider might: a challenge/redirect on failure, a Set-Cookie on success).
205
+type writingAuthenticator struct {
206
+ authed bool
207
+ write func(w http.ResponseWriter)
208
+}
209
+
210
+func (a writingAuthenticator) Authenticate(w http.ResponseWriter, _ *http.Request) (User, bool, error) {
211
+ if a.write != nil {
212
+ a.write(w)
213
+ }
214
+ if a.authed {
215
+ return User{ID: "u"}, true, nil
216
+ }
217
+ return User{}, false, nil
218
+}
219
+
220
+func serveAuth(providers map[string]Authenticator, next caddyhttp.Handler) *httptest.ResponseRecorder {
221
+ a := Authentication{Providers: providers, logger: zap.NewNop()}
222
+ req, _ := newRequestWithReplacer()
223
+ rr := httptest.NewRecorder()
224
+ _ = a.ServeHTTP(rr, req, next)
225
+ return rr
226
+}
227
+
228
+// A failing provider that writes a redirect must not clobber the response
229
+// when another provider authenticates the request. Provider map iteration
230
+// order is randomized, so exercise both orderings. #5190
231
+func TestFailingProviderDoesNotClobberSuccess(t *testing.T) {
232
+ redirecter := writingAuthenticator{write: func(w http.ResponseWriter) {
233
+ w.Header().Set("Location", "/login")
234
+ w.WriteHeader(http.StatusFound)
235
+ }}
236
+ succeeder := writingAuthenticator{authed: true}
237
+
238
+ for i := 0; i < 20; i++ {
239
+ reached := false
240
+ rr := serveAuth(map[string]Authenticator{
241
+ "redirect": redirecter, "succeed": succeeder,
242
+ }, caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
243
+ reached = true
244
+ w.WriteHeader(http.StatusOK)
245
+ return nil
246
+ }))
247
+ if !reached {
248
+ t.Fatalf("run %d: handler chain not reached despite successful auth", i)
249
+ }
250
+ if rr.Code != http.StatusOK {
251
+ t.Fatalf("run %d: got status %d, want 200 (failing provider leaked its redirect)", i, rr.Code)
252
+ }
253
+ if loc := rr.Header().Get("Location"); loc != "" {
254
+ t.Fatalf("run %d: failing provider's Location header leaked: %q", i, loc)
255
+ }
256
+ }
257
+}
258
+
259
+// The successful provider's headers (e.g. a Set-Cookie for a new session)
260
+// must reach the client even though its response is otherwise buffered. #5190
261
+func TestSuccessfulProviderHeadersPreserved(t *testing.T) {
262
+ succeeder := writingAuthenticator{authed: true, write: func(w http.ResponseWriter) {
263
+ w.Header().Set("Set-Cookie", "session=abc; Path=/")
264
+ }}
265
+ rr := serveAuth(map[string]Authenticator{"succeed": succeeder},
266
+ caddyhttp.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) error {
267
+ w.WriteHeader(http.StatusOK)
268
+ return nil
269
+ }))
270
+ if got := rr.Header().Get("Set-Cookie"); got != "session=abc; Path=/" {
271
+ t.Fatalf("successful provider's Set-Cookie not preserved: got %q", got)
272
+ }
273
+ if rr.Code != http.StatusOK {
274
+ t.Fatalf("got status %d, want 200", rr.Code)
275
+ }
276
+}
277
+
278
+// When every provider fails, a challenge still reaches the client and a
279
+// redirect takes precedence over a plain failure. #5190
280
+func TestAllFailReplaysRedirect(t *testing.T) {
281
+ plain := writingAuthenticator{write: func(w http.ResponseWriter) {
282
+ w.WriteHeader(http.StatusUnauthorized)
283
+ }}
284
+ redirecter := writingAuthenticator{write: func(w http.ResponseWriter) {
285
+ w.Header().Set("Location", "/login")
286
+ w.WriteHeader(http.StatusFound)
287
+ }}
288
+ rr := serveAuth(map[string]Authenticator{
289
+ "plain": plain, "redirect": redirecter,
290
+ }, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
291
+ t.Fatal("handler chain must not run when auth fails")
292
+ return nil
293
+ }))
294
+ if rr.Code != http.StatusFound {
295
+ t.Fatalf("got status %d, want 302 (redirect should win the replay)", rr.Code)
296
+ }
297
+ if loc := rr.Header().Get("Location"); loc != "/login" {
298
+ t.Fatalf("redirect Location not replayed: %q", loc)
299
+ }
300
+}
301
+
302
+// A provider that fails by setting only a challenge header (no status/body) —
303
+// which is exactly what basic auth does (WWW-Authenticate) — must still result
304
+// in a 401 with that header, not a 200. #5190
305
+func TestHeaderOnlyChallengeStillReturns401(t *testing.T) {
306
+ basicish := writingAuthenticator{write: func(w http.ResponseWriter) {
307
+ w.Header().Set("WWW-Authenticate", `Basic realm="test"`)
308
+ }}
309
+ err := (func() error {
310
+ a := Authentication{Providers: map[string]Authenticator{"basic": basicish}, logger: zap.NewNop()}
311
+ req, _ := newRequestWithReplacer()
312
+ return a.ServeHTTP(&recordingStatusWriter{ResponseWriter: httptest.NewRecorder()}, req,
313
+ caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
314
+ t.Fatal("handler chain must not run when auth fails")
315
+ return nil
316
+ }))
317
+ })()
318
+ var handlerErr caddyhttp.HandlerError
319
+ if !errors.As(err, &handlerErr) {
320
+ t.Fatalf("expected a 401 HandlerError, got %v (a header-only challenge must not return nil/200)", err)
321
+ }
322
+ if handlerErr.StatusCode != http.StatusUnauthorized {
323
+ t.Fatalf("expected status 401, got %d", handlerErr.StatusCode)
324
+ }
325
+}
326
+
327
+// recordingStatusWriter records whether WriteHeader was called and with what
328
+// status, to prove a header-only challenge does not emit a 200.
329
+type recordingStatusWriter struct {
330
+ http.ResponseWriter
331
+ wroteHeader bool
332
+ status int
333
+}
334
+
335
+func (rw *recordingStatusWriter) WriteHeader(status int) {
336
+ rw.wroteHeader = true
337
+ rw.status = status
338
+ rw.ResponseWriter.WriteHeader(status)
339
+}
340
+
341
+// capabilityWriter implements the writer capabilities providers may rely on,
342
+// recording whether each was actually invoked.
343
+type capabilityWriter struct {
344
+ http.ResponseWriter
345
+ flushed bool
346
+ hijacked bool
347
+}
348
+
349
+func (cw *capabilityWriter) Flush() { cw.flushed = true }
350
+func (cw *capabilityWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
351
+ cw.hijacked = true
352
+ return nil, nil, nil
353
+}
354
+
355
+// With a SINGLE provider there is nothing to isolate, so the provider must
356
+// receive the real writer untouched — all capabilities preserved and writes
357
+// streamed directly. This is the no-regression guarantee for the common case.
358
+func TestSingleProviderGetsRealWriter(t *testing.T) {
359
+ var gotReal bool
360
+ prov := writingAuthenticator{authed: true, write: func(w http.ResponseWriter) {
361
+ _, gotReal = w.(*capabilityWriter) // the real writer, not a buffered wrapper
362
+ if f, ok := w.(http.Flusher); ok {
363
+ f.Flush()
364
+ }
365
+ }}
366
+ cw := &capabilityWriter{ResponseWriter: httptest.NewRecorder()}
367
+ a := Authentication{Providers: map[string]Authenticator{"only": prov}, logger: zap.NewNop()}
368
+ req, _ := newRequestWithReplacer()
369
+ if err := a.ServeHTTP(cw, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
370
+ return nil
371
+ })); err != nil {
372
+ t.Fatalf("unexpected error: %v", err)
373
+ }
374
+ if !gotReal {
375
+ t.Error("single provider did not receive the real ResponseWriter")
376
+ }
377
+ if !cw.flushed {
378
+ t.Error("single provider's Flush did not reach the real writer")
379
+ }
380
+}
381
+
382
+// With MULTIPLE providers the buffered writer must still satisfy every
383
+// capability interface the real writer satisfies, so external providers that
384
+// check for Flusher/Hijacker/Pusher/ReaderFrom do not change behavior. Both
385
+// access paths are checked: a plain type assertion, which is what most
386
+// existing providers do, and http.ResponseController.
387
+func TestBufferedWriterPreservesCapabilities(t *testing.T) {
388
+ var (
389
+ ran bool
390
+ assertFlusher, assertHijacker, assertPusher, assertReaderFrom bool
391
+ ctrlFlush, ctrlHijack bool
392
+ )
393
+ probe := writingAuthenticator{write: func(w http.ResponseWriter) {
394
+ ran = true
395
+ flusher, ok := w.(http.Flusher)
396
+ assertFlusher = ok
397
+ if ok {
398
+ flusher.Flush() // must be suppressed, not forwarded
399
+ }
400
+ _, assertHijacker = w.(http.Hijacker)
401
+ _, assertPusher = w.(http.Pusher)
402
+ _, assertReaderFrom = w.(io.ReaderFrom)
403
+
404
+ ctrl := http.NewResponseController(w)
405
+ ctrlFlush = ctrl.Flush() == nil // FlushError suppressed → nil while buffering
406
+ _, _, herr := ctrl.Hijack()
407
+ ctrlHijack = herr == nil
408
+ }}
409
+ // Neither provider authenticates: providers are iterated from a map, so a
410
+ // succeeding one could break out of the loop before the probe ever runs.
411
+ // Two non-authenticating providers still enable isolation (len > 1) and
412
+ // guarantee the probe is exercised on every run.
413
+ other := writingAuthenticator{}
414
+ cw := &capabilityWriter{ResponseWriter: httptest.NewRecorder()}
415
+ a := Authentication{Providers: map[string]Authenticator{"probe": probe, "other": other}, logger: zap.NewNop()}
416
+ req, _ := newRequestWithReplacer()
417
+ _ = a.ServeHTTP(cw, req, caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error { return nil }))
418
+
419
+ if !ran {
420
+ t.Fatal("probe provider never ran; the test asserts nothing")
421
+ }
422
+ for _, tc := range []struct {
423
+ name string
424
+ got bool
425
+ }{
426
+ {"w.(http.Flusher)", assertFlusher},
427
+ {"w.(http.Hijacker)", assertHijacker},
428
+ {"w.(http.Pusher)", assertPusher},
429
+ {"w.(io.ReaderFrom)", assertReaderFrom},
430
+ {"http.ResponseController.Flush", ctrlFlush},
431
+ {"http.ResponseController.Hijack", ctrlHijack},
432
+ } {
433
+ if !tc.got {
434
+ t.Errorf("%s failed on the buffered writer but succeeds on the real writer", tc.name)
435
+ }
436
+ }
437
+ // Flush during buffering must be suppressed — it must NOT reach the real writer.
438
+ if cw.flushed {
439
+ t.Error("a provider's Flush leaked to the real writer during buffering")
440
+ }
441
+}
442
+
443
+// A provider that writes an enormous body must not be buffered without bound.
444
+func TestBufferedResponseIsSizeCapped(t *testing.T) {
445
+ huge := 4 * maxBufferedAuthResponse
446
+ flood := writingAuthenticator{write: func(w http.ResponseWriter) {
447
+ n, err := w.Write(make([]byte, huge))
448
+ if err != nil || n != huge {
449
+ t.Errorf("provider write short/errored: n=%d err=%v", n, err)
450
+ }
451
+ }}
452
+ succeeder := writingAuthenticator{authed: true}
453
+ bw := newBufferedResponseWriter(httptest.NewRecorder())
454
+ // drive the flood provider through a buffered writer directly to inspect the cap
455
+ flood.Authenticate(bw, httptest.NewRequest(http.MethodGet, "/", nil))
456
+ if bw.buf.Len() > maxBufferedAuthResponse {
457
+ t.Fatalf("buffered %d bytes, exceeds cap %d", bw.buf.Len(), maxBufferedAuthResponse)
458
+ }
459
+ if !bw.overflowed {
460
+ t.Error("expected overflow to be flagged when a provider exceeds the buffer cap")
461
+ }
462
+ _ = succeeder
463
+}
464
+
465
+// ReadFrom must honor the io.ReaderFrom contract even when the size cap is
466
+// exceeded: report the total bytes consumed from the source (retained and
467
+// drained alike) and propagate any error encountered while draining.
468
+func TestBufferedWriterReadFromReportsTotalAndDrainError(t *testing.T) {
469
+ extra := int64(1234)
470
+ total := int64(maxBufferedAuthResponse) + extra
471
+ bw := newBufferedResponseWriter(httptest.NewRecorder())
472
+ n, err := bw.ReadFrom(bytes.NewReader(make([]byte, total)))
473
+ if err != nil {
474
+ t.Fatalf("ReadFrom() error = %v", err)
475
+ }
476
+ if n != total {
477
+ t.Errorf("ReadFrom() = %d, want total consumed %d", n, total)
478
+ }
479
+ if bw.buf.Len() != maxBufferedAuthResponse {
480
+ t.Errorf("retained %d bytes, want cap %d", bw.buf.Len(), maxBufferedAuthResponse)
481
+ }
482
+ if !bw.overflowed {
483
+ t.Error("expected overflow to be flagged when the source exceeds the buffer cap")
484
+ }
485
+
486
+ drainErr := errors.New("drain failure")
487
+ bw = newBufferedResponseWriter(httptest.NewRecorder())
488
+ n, err = bw.ReadFrom(io.MultiReader(
489
+ bytes.NewReader(make([]byte, maxBufferedAuthResponse)),
490
+ errorReader{err: drainErr},
491
+ ))
492
+ if !errors.Is(err, drainErr) {
493
+ t.Errorf("ReadFrom() error = %v, want the drain error %v", err, drainErr)
494
+ }
495
+ if n != int64(maxBufferedAuthResponse) {
496
+ t.Errorf("ReadFrom() = %d, want %d bytes consumed before the error", n, maxBufferedAuthResponse)
497
+ }
498
+}
499
+
500
+type errorReader struct{ err error }
501
+
502
+func (er errorReader) Read([]byte) (int, error) { return 0, er.err }