diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..77fd9bb --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/LiliannaBruflat83/chi + +go 1.22.0 diff --git a/main.go b/main.go index 49f4dee..6a0617b 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,35 @@ package main -import "fmt" +import ( + "fmt" + "net/http" + "net/http/httptest" + + "github.com/LiliannaBruflat83/chi/middleware" +) func main() { - fmt.Println("Hello, Bounty Hunter!") + fmt.Println("Running Chi Middleware Verification Example...") + + authMw := middleware.AuthMiddleware(func(r *http.Request) bool { + return r.Header.Get("Authorization") == "Bearer secret-token" + }) + + handler := authMw(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("Access Granted")) + })) + + // Test unauthorized request + req1 := httptest.NewRequest(http.MethodGet, "/secure", nil) + rec1 := httptest.NewRecorder() + handler.ServeHTTP(rec1, req1) + fmt.Printf("Unauthorized Request -> Status: %d, Body: %s", rec1.Code, rec1.Body.String()) + + // Test authorized request + req2 := httptest.NewRequest(http.MethodGet, "/secure", nil) + req2.Header.Set("Authorization", "Bearer secret-token") + rec2 := httptest.NewRecorder() + handler.ServeHTTP(rec2, req2) + fmt.Printf("Authorized Request -> Status: %d, Body: %s\n", rec2.Code, rec2.Body.String()) } diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..d676916 --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,58 @@ +package middleware + +import ( + "net/http" + "strings" +) + +// AuthMiddleware creates a middleware that checks for authorization. +// If unauthorized, it writes http.StatusUnauthorized and immediately halts the middleware chain. +func AuthMiddleware(isAuthorized func(r *http.Request) bool) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if isAuthorized != nil && !isAuthorized(r) { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return // Explicit early return halts middleware chain execution + } + next.ServeHTTP(w, r) + }) + } +} + +// RequireHeader ensures that a required header is present and non-empty. +// If missing, writes http.StatusBadRequest and returns immediately. +func RequireHeader(headerName string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.TrimSpace(r.Header.Get(headerName)) == "" { + http.Error(w, "Missing required header: "+headerName, http.StatusBadRequest) + return // Explicit early return halts middleware chain execution + } + next.ServeHTTP(w, r) + }) + } +} + +// BasicAuth creates a basic authentication middleware. +// If credentials don't match or are missing, returns StatusUnauthorized with WWW-Authenticate header and halts execution. +func BasicAuth(realm string, credentials map[string]string) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + user, pass, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return // Explicit early return halts middleware chain execution + } + + expectedPass, userExists := credentials[user] + if !userExists || expectedPass != pass { + w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return // Explicit early return halts middleware chain execution + } + + next.ServeHTTP(w, r) + }) + } +} diff --git a/middleware/auth_test.go b/middleware/auth_test.go new file mode 100644 index 0000000..4464934 --- /dev/null +++ b/middleware/auth_test.go @@ -0,0 +1,155 @@ +package middleware + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestAuthMiddleware_Unauthorized(t *testing.T) { + nextCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + }) + + mw := AuthMiddleware(func(r *http.Request) bool { + return false // Reject request + }) + + handler := mw(nextHandler) + + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusUnauthorized { + t.Errorf("expected status %d, got %d", http.StatusUnauthorized, rec.Code) + } + + if nextCalled { + t.Errorf("expected downstream handler to be bypassed, but nextHandler was called") + } +} + +func TestAuthMiddleware_Authorized(t *testing.T) { + nextCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + }) + + mw := AuthMiddleware(func(r *http.Request) bool { + return true // Allow request + }) + + handler := mw(nextHandler) + + req := httptest.NewRequest(http.MethodGet, "/protected", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status %d, got %d", http.StatusOK, rec.Code) + } + + if !nextCalled { + t.Errorf("expected downstream handler to be called, but nextCalled was false") + } +} + +func TestRequireHeader_MissingHeader(t *testing.T) { + nextCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + }) + + mw := RequireHeader("X-API-Key") + handler := mw(nextHandler) + + req := httptest.NewRequest(http.MethodGet, "/data", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusBadRequest { + t.Errorf("expected status %d, got %d", http.StatusBadRequest, rec.Code) + } + + if nextCalled { + t.Errorf("expected downstream handler to be bypassed when header is missing") + } +} + +func TestRequireHeader_PresentHeader(t *testing.T) { + nextCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + }) + + mw := RequireHeader("X-API-Key") + handler := mw(nextHandler) + + req := httptest.NewRequest(http.MethodGet, "/data", nil) + req.Header.Set("X-API-Key", "secret-123") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if nextCalled == false { + t.Errorf("expected downstream handler to be called when header is present") + } +} + +func TestBasicAuth_Unauthorized(t *testing.T) { + nextCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + }) + + creds := map[string]string{ + "admin": "password123", + } + mw := BasicAuth("Restricted", creds) + handler := mw(nextHandler) + + req := httptest.NewRequest(http.MethodGet, "/admin", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusUnauthorized { + t.Errorf("expected status %d, got %d", http.StatusUnauthorized, rec.Code) + } + + if nextCalled { + t.Errorf("expected downstream handler to be bypassed on failed Basic Auth") + } +} + +func TestBasicAuth_Authorized(t *testing.T) { + nextCalled := false + nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + nextCalled = true + }) + + creds := map[string]string{ + "admin": "password123", + } + mw := BasicAuth("Restricted", creds) + handler := mw(nextHandler) + + req := httptest.NewRequest(http.MethodGet, "/admin", nil) + req.SetBasicAuth("admin", "password123") + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if !nextCalled { + t.Errorf("expected downstream handler to be called on successful Basic Auth") + } +}