diff --git a/auth/auth.go b/auth/auth.go
new file mode 100644
index 0000000000000000000000000000000000000000..ce6dab6f6dd317865a04204959363cf7c3eb8e65
--- /dev/null
+++ b/auth/auth.go
@@ -0,0 +1,49 @@
+package auth
+
+import (
+	"context"
+	"net/http"
+)
+
+type contextKey string
+
+type Provider interface {
+	// Returns true if pass is a valid password for the given user
+	Valid(user, pass string) bool
+}
+
+// Require wraps hdlr so that it requires authentication to use. Requests handled by hdlr will have the user
+// name attached to their context. Use the User function to retrieve it.
+func Require(hdlr http.HandlerFunc, provider Provider) http.HandlerFunc {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		user, pass, ok := r.BasicAuth()
+		if !ok {
+			w.Header().Add("WWW-Authenticate", `Basic realm="In Vino Veritas"`)
+			w.WriteHeader(http.StatusUnauthorized)
+
+			return
+		}
+
+		if !provider.Valid(user, pass) {
+			w.Header().Add("WWW-Authenticate", `Basic realm="In Vino Veritas"`)
+			w.WriteHeader(http.StatusUnauthorized)
+
+			return
+		}
+
+		key := contextKey("user")
+		ctx := context.WithValue(r.Context(), key, user)
+
+		hdlr(w, r.WithContext(ctx))
+	})
+}
+
+// User returns the user from r's context. If r has no user in its context, the empty string will be returned.
+func User(r *http.Request) string {
+	val := r.Context().Value(contextKey("user"))
+	if val == nil {
+		return ""
+	}
+
+	return val.(string)
+}
diff --git a/handler-details.go b/handler-details.go
index 634627a5b1d359e208feabe5456dc1659bdfae0c..932f44c7a44991396f9241ea4eafd986ca07ed2c 100644
--- a/handler-details.go
+++ b/handler-details.go
@@ -15,7 +15,7 @@ import (
 var detailsTemplate = template.Must(template.ParseFS(templateFS, "templates/base.tpl", "templates/details.tpl"))
 
 func (h Handler) img(w http.ResponseWriter, r *http.Request) {
-	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr)
+	logRequest(r)
 
 	if r.Method != "GET" {
 		httpError(w, "bad method", errors.New(r.Method), http.StatusMethodNotAllowed)
diff --git a/handler-index.go b/handler-index.go
index 2beab742b4fb89b93301027c1abbe1ca48463883..fa35bc55911e79408dc406bc11254bbc8adddb4f 100644
--- a/handler-index.go
+++ b/handler-index.go
@@ -17,7 +17,7 @@ func (h Handler) index(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr)
+	logRequest(r)
 
 	if r.Method == "GET" {
 		wines, err := ListWines(r.Context(), h.db)
diff --git a/main.go b/main.go
index 0ab495736f39a9fca9f9b43580c59c969190970d..0cd87131dc45e52a5175fd1cfa409068b01b925a 100644
--- a/main.go
+++ b/main.go
@@ -7,6 +7,8 @@ import (
 
 	"github.com/jmoiron/sqlx"
 	_ "modernc.org/sqlite" // Imported for side effects: registers DB driver
+
+	"git.c3pb.de/gbe/invinoveritas/auth"
 )
 
 //go:embed templates/*.tpl
@@ -29,6 +31,20 @@ func httpError(w http.ResponseWriter, msg string, err error, status int) {
 	http.Error(w, msg, status)
 }
 
+type authProvider struct{}
+
+func (a authProvider) Valid(user, pass string) bool {
+	if user == "wine" && pass == "potatoe" {
+		return true
+	}
+
+	return false
+}
+
+func logRequest(r *http.Request) {
+	log.Println("handling", r.Method, r.URL, "from", r.RemoteAddr, "by", auth.User(r))
+}
+
 func main() {
 	db, err := sqlx.Open("sqlite", "vino.sqlite")
 	if err != nil {
@@ -41,15 +57,19 @@ func main() {
 		log.Fatalln("can't initialize DB:", err)
 	}
 
+	http.HandleFunc("/favicon.ico", http.NotFound)
+
 	http.Handle("/static/", http.FileServer(http.FS(staticFS)))
 
 	handler := Handler{
 		db: db,
 	}
 
-	http.HandleFunc("/details/img", handler.img)
-	http.HandleFunc("/details/", handler.details)
-	http.HandleFunc("/", handler.index)
+	ap := authProvider{}
+
+	http.HandleFunc("/details/img", auth.Require(http.HandlerFunc(handler.img), ap))
+	http.HandleFunc("/details/", auth.Require(http.HandlerFunc(handler.details), ap))
+	http.HandleFunc("/", auth.Require(http.HandlerFunc(handler.index), ap))
 
 	const listenAddr = ":7878"