diff options
| author | roerick <roerick@wyo.town> | 2025-10-19 14:24:40 -0600 |
|---|---|---|
| committer | roerick <roerick@wyo.town> | 2025-10-19 14:24:40 -0600 |
| commit | 132e0fe607396123a9fc5390c8657ef145bba3c6 (patch) | |
| tree | 9a420d6f89a2de70d17bfb08789a26ccc501a9b9 | |
| parent | a1c3883334a9c54358173ebd822a5a85036dcda6 (diff) | |
initial commit with auth working
| -rw-r--r-- | bsdauth/bsdauth.go | 48 | ||||
| -rw-r--r-- | bsdauth/bsdauth_stub.go | 11 | ||||
| -rw-r--r-- | db/db.go | 9 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | handlers/login.go | 70 | ||||
| -rw-r--r-- | main.go | 46 | ||||
| -rw-r--r-- | middleware/auth.go | 17 | ||||
| -rw-r--r-- | models/models.go | 2 | ||||
| -rw-r--r-- | session/session.go | 24 |
10 files changed, 216 insertions, 14 deletions
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 <stdlib.h> +#include <sys/types.h> +#include <login_cap.h> +#include <bsd_auth.h> + +// 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") + } +} @@ -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 @@ -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(` +<!doctype html><title>Login</title> +<form method="post" action="/login/submit"> + <label>User <input name="username" autocomplete="username"></label><br> + <label>Pass <input name="password" type="password" autocomplete="current-password"></label><br> + <button>Login</button> +</form> +`)) + +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) +} @@ -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 +} + |
