From 132e0fe607396123a9fc5390c8657ef145bba3c6 Mon Sep 17 00:00:00 2001 From: roerick Date: Sun, 19 Oct 2025 14:24:40 -0600 Subject: initial commit with auth working --- bsdauth/bsdauth.go | 48 +++++++++++++++++++++++++++++++++ bsdauth/bsdauth_stub.go | 11 ++++++++ db/db.go | 9 +++++++ go.mod | 1 + go.sum | 2 ++ handlers/login.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 46 +++++++++++++++++++++++--------- middleware/auth.go | 17 ++++++++++++ models/models.go | 2 -- session/session.go | 24 +++++++++++++++++ 10 files changed, 216 insertions(+), 14 deletions(-) create mode 100644 bsdauth/bsdauth.go create mode 100644 bsdauth/bsdauth_stub.go create mode 100644 db/db.go create mode 100644 handlers/login.go create mode 100644 middleware/auth.go create mode 100644 session/session.go diff --git a/bsdauth/bsdauth.go b/bsdauth/bsdauth.go new file mode 100644 index 0000000..6105360 --- /dev/null +++ b/bsdauth/bsdauth.go @@ -0,0 +1,48 @@ +//go:build openbsd && cgo + +package bsdauth + +/* +#cgo CFLAGS: -DOPENBSD +#cgo LDFLAGS: -lutil +#include +#include +#include +#include + +// Tiny wrapper to keep the cgo call site clean. +static int go_auth_userokay(const char *name, const char *style, const char *atype, const char *password) { + return auth_userokay((char*)name, (char*)style, (char*)atype, (char*)password); +} +*/ +import "C" +import ( + "errors" + "unsafe" +) + +// Authenticate verifies a username and password using OpenBSD’s BSD Authentication subsystem. +// `atype` identifies the service name (e.g., "auth-myapp"). +// `style` is left empty to use the system default style from login.conf. +func Authenticate(username, password, atype string) (bool, error) { + if username == "" { + return false, errors.New("bsdauth: empty username") + } + if atype == "" { + atype = "auth-myapp" + } + + cUser := C.CString(username) + cPass := C.CString(password) + cType := C.CString(atype) + var cStyle *C.char = nil // default style from login.conf + defer func() { + C.free(unsafe.Pointer(cUser)) + C.free(unsafe.Pointer(cPass)) + C.free(unsafe.Pointer(cType)) + }() + + ok := C.go_auth_userokay(cUser, cStyle, cType, cPass) + return ok != 0, nil +} + diff --git a/bsdauth/bsdauth_stub.go b/bsdauth/bsdauth_stub.go new file mode 100644 index 0000000..95315f3 --- /dev/null +++ b/bsdauth/bsdauth_stub.go @@ -0,0 +1,11 @@ +//go:build !openbsd || !cgo + +package bsdauth + +import "errors" + +// Authenticate is a stub for non-OpenBSD platforms. +func Authenticate(username, password, atype string) (bool, error) { + return false, errors.New("bsdauth: unsupported platform (requires OpenBSD + cgo)") +} + diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..766d15e --- /dev/null +++ b/db/db.go @@ -0,0 +1,9 @@ +package db + +function InitDB () { + dsn := "host=localhost user=_postgresql password=postGRES dbname=netzah port=5432 sslmode=disable TimeZone=US/Mountain client_encoding=UTF8" + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) + if err != nil { + panic("failed to connect database") + } +} diff --git a/go.mod b/go.mod index fd32880..b449be3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module wyo.town/netzah go 1.24.1 require ( + github.com/alexedwards/scs/v2 v2.9.0 github.com/gorilla/mux v1.8.1 gorm.io/driver/postgres v1.6.0 gorm.io/gorm v1.31.0 diff --git a/go.sum b/go.sum index 6b3246d..e2f09bb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alexedwards/scs/v2 v2.9.0 h1:xa05mVpwTBm1iLeTMNFfAWpKUm4fXAW7CeAViqBVS90= +github.com/alexedwards/scs/v2 v2.9.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/handlers/login.go b/handlers/login.go new file mode 100644 index 0000000..831cd36 --- /dev/null +++ b/handlers/login.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "net/http" + "text/template" + "context" + + "wyo.town/netzah/bsdauth" + "wyo.town/netzah/session" + ) + + +var loginTmpl = template.Must(template.New("login").Parse(` +Login +
+
+
+ +
+`)) + +func LoginGET(w http.ResponseWriter, r *http.Request) { + _ = loginTmpl.Execute(w, nil) +} + +func LoginPOST(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + // Parse credentials + if err := r.ParseForm(); err != nil { + http.Error(w, "bad form", http.StatusBadRequest) + return + } + user := r.Form.Get("username") + pass := r.Form.Get("password") + + // One-time BSD Auth (OpenBSD's BSD Authentication, not PAM) + ok, _ := bsdauth.Authenticate(user, pass, "auth-myapp") + // `auth_userokay()` returns nonzero on success. :contentReference[oaicite:2]{index=2} + if !ok { + http.Error(w, "invalid credentials", http.StatusUnauthorized) + return + } + + // Rotate session token to prevent fixation, then set user + if err := session.Manager.RenewToken(r.Context()); err != nil { + http.Error(w, "server error", http.StatusInternalServerError) + return + } + session.Manager.Put(r.Context(), "user", user) + + http.Redirect(w, r, "/", http.StatusSeeOther) +} + +func LogoutPOST(w http.ResponseWriter, r *http.Request) { + _ = session.Manager.Destroy(r.Context()) + http.Redirect(w, r, "/login", http.StatusSeeOther) +} + +func Dashboard(w http.ResponseWriter, r *http.Request) { + user := session.Manager.GetString(r.Context(), "user") + w.Write([]byte("Hello, " + user + "!")) +} + +// Helper to stash values if you ever need it +func withUser(ctx context.Context, user string) context.Context { + return context.WithValue(ctx, struct{ k string }{"user"}, user) +} diff --git a/main.go b/main.go index dbccf98..f381fa8 100644 --- a/main.go +++ b/main.go @@ -2,11 +2,12 @@ package main import ( "log" - "fmt" "net/http" "wyo.town/netzah/models" "wyo.town/netzah/handlers" + "wyo.town/netzah/session" + "wyo.town/netzah/middleware" "github.com/gorilla/mux" "gorm.io/driver/postgres" @@ -19,27 +20,48 @@ func init() { if err != nil { panic("failed to connect database") } - db.AutoMigrate(&models.User{}) db.AutoMigrate(&models.Blog{}) - db.AutoMigrate(&models.Comment{}) } + + + func main() { + + session.Init() + // 2) Router + attach SCS middleware correctly r := mux.NewRouter() + r.Use(session.Manager.LoadAndSave) + // 3) Routes r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) - w.Write([]byte("Hello, World!")) + _, _ = w.Write([]byte("Hello, World!")) }) // Blog endpoints - r.HandleFunc("/blogs", handlers.CreateBlog).Methods("GET") - r.HandleFunc("/blogs", handlers.StoreBlog).Methods("POST") - r.HandleFunc("/blogs/{id}", handlers.GetBlog).Methods("GET") - // r.HandleFunc("/blogs/{id}", handlers.UpdateBlog).Methods("PUT") - // r.HandleFunc("/blogs/{id}", handlers.DeleteBlog).Methods("DELETE") // Also consider adding to handle a form submission for deletion - fmt.Println("Starting server on :8090") - if err := http.ListenAndServe(":8090", r); err != nil { - log.Fatal(err) + r.HandleFunc("/blogs", handlers.CreateBlog).Methods(http.MethodGet) + r.HandleFunc("/blogs", handlers.StoreBlog).Methods(http.MethodPost) + r.HandleFunc("/blogs/{id}", handlers.GetBlog).Methods(http.MethodGet) + + r.HandleFunc("/login", handlers.LoginGET) + r.HandleFunc("/login/submit", handlers.LoginPOST) + + // Logout (POST is recommended; allow GET only if you must) + r.HandleFunc("/logout", handlers.LogoutPOST) + + // r.HandleFunc("/blogs/{id}", handlers.UpdateBlog).Methods(http.MethodPut) + // r.HandleFunc("/blogs/{id}", handlers.DeleteBlog).Methods(http.MethodDelete) + + // Example protected route (SCS must already be on the chain) + r.Handle("/whoami", middleware.RequireAuth(http.HandlerFunc(handlers.Dashboard))) + + // 4) Start the server using the router you wrapped + log.Println("Starting server on :8090") + srv := &http.Server{ + Addr: ":8090", + Handler: r, } + log.Fatal(srv.ListenAndServe()) } + diff --git a/middleware/auth.go b/middleware/auth.go new file mode 100644 index 0000000..947985a --- /dev/null +++ b/middleware/auth.go @@ -0,0 +1,17 @@ +package middleware + +import ( + "net/http" + + "wyo.town/netzah/session" +) + +func RequireAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !session.Manager.Exists(r.Context(), "user") { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + next.ServeHTTP(w, r) + }) +} diff --git a/models/models.go b/models/models.go index 355851a..b36cc59 100644 --- a/models/models.go +++ b/models/models.go @@ -5,7 +5,5 @@ type Blog struct { Title string `gorm:"not null" json:"title"` Content string `gorm:"not null" json:"content"` UserID uint `gorm:"not null" json:"user_id"` - User User `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"user"` - Comments []Comment `gorm:"foreignKey:BlogID" json:"comments"` } diff --git a/session/session.go b/session/session.go new file mode 100644 index 0000000..d80e4f9 --- /dev/null +++ b/session/session.go @@ -0,0 +1,24 @@ +package session + +import ( + "time" + "net/http" + + "github.com/alexedwards/scs/v2" +) + +var Manager *scs.SessionManager + +func Init() { + sm := scs.New() + // Security/behavior + sm.IdleTimeout = 20 * time.Minute // logout after inactivity + sm.Lifetime = 12 * time.Hour // absolute expiry + sm.Cookie.Name = "sid" + sm.Cookie.HttpOnly = true + sm.Cookie.SameSite = http.SameSiteLaxMode + sm.Cookie.Secure = true // set false only on localhost HTTP during dev + + Manager = sm +} + -- cgit v1.2.3