auth: include Basic challenge in 401 responses

Requests rejected by Basic authentication currently return 401 without WWW-Authenticate, preventing HTTP clients from prompting for credentials. Advertise the Basic scheme on the server and metrics auth paths while leaving proxy authentication unchanged.
This commit is contained in:
Alpha
2026-10-07 06:16:54 +08:00
parent ca666baf9c
commit 9d98cbd03f
4 changed files with 70 additions and 2 deletions
+6
View File
@@ -0,0 +1,6 @@
Bugfix: Include a Basic authentication challenge when credentials are rejected
Requests rejected by Basic authentication now include a `WWW-Authenticate` header,
allowing HTTP clients to prompt for credentials.
https://github.com/restic/rest-server/issues/376
+1 -1
View File
@@ -59,7 +59,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// First of all, check auth (will always pass if NoAuth is set) // First of all, check auth (will always pass if NoAuth is set)
username, ok := s.checkAuth(r) username, ok := s.checkAuth(r)
if !ok { if !ok {
httpDefaultError(w, http.StatusUnauthorized) s.writeUnauthorized(w)
return return
} }
+8 -1
View File
@@ -56,11 +56,18 @@ func (s *Server) checkAuth(r *http.Request) (username string, ok bool) {
return username, true return username, true
} }
func (s *Server) writeUnauthorized(w http.ResponseWriter) {
if !s.NoAuth && s.ProxyAuthUsername == "" {
w.Header().Set("WWW-Authenticate", `Basic realm="rest-server"`)
}
httpDefaultError(w, http.StatusUnauthorized)
}
func (s *Server) wrapMetricsAuth(f http.HandlerFunc) http.HandlerFunc { func (s *Server) wrapMetricsAuth(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
username, ok := s.checkAuth(r) username, ok := s.checkAuth(r)
if !ok { if !ok {
httpDefaultError(w, http.StatusUnauthorized) s.writeUnauthorized(w)
return return
} }
if s.PrivateRepos && username != "metrics" { if s.PrivateRepos && username != "metrics" {
+55
View File
@@ -1,6 +1,7 @@
package restserver package restserver
import ( import (
"net/http"
"net/http/httptest" "net/http/httptest"
"testing" "testing"
) )
@@ -78,3 +79,57 @@ func TestCheckAuth(t *testing.T) {
}) })
} }
} }
func TestUnauthorizedResponsesIncludeBasicChallenge(t *testing.T) {
const challenge = `Basic realm="rest-server"`
tests := []struct {
name string
server *Server
metrics bool
wantChallenge string
}{
{
name: "server Basic authentication",
server: &Server{},
wantChallenge: challenge,
},
{
name: "server proxy authentication",
server: &Server{ProxyAuthUsername: "X-Remote-User"},
},
{
name: "metrics Basic authentication",
server: &Server{},
metrics: true,
wantChallenge: challenge,
},
{
name: "metrics proxy authentication",
server: &Server{ProxyAuthUsername: "X-Remote-User"},
metrics: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var handler http.HandlerFunc
if tt.metrics {
handler = tt.server.wrapMetricsAuth(func(http.ResponseWriter, *http.Request) {
t.Fatal("handler should not be called for an unauthorized request")
})
} else {
handler = tt.server.ServeHTTP
}
rec := httptest.NewRecorder()
handler(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != http.StatusUnauthorized {
t.Fatalf("status = %d, want %d", rec.Code, http.StatusUnauthorized)
}
if got := rec.Header().Get("WWW-Authenticate"); got != tt.wantChallenge {
t.Errorf("WWW-Authenticate = %q, want %q", got, tt.wantChallenge)
}
})
}
}