git.sophuwu.com > goauth
htsesh for http session :>
sophuwu sophie@skisiel.com
Sun, 18 May 2025 18:45:36 +0200
commit

5abdbba5384c2d9f919552c16dcd0b3b100efbe3

parent

29d5e5f23745f8a8265d57cf700f866c0c81adcd

3 files changed, 351 insertions(+), 218 deletions(-)

jump to
A htsesh/http.go

@@ -0,0 +1,219 @@

+package htsesh + +// +// import ( +// "crypto/rand" +// "crypto/sha1" +// "crypto/sha512" +// "crypto/subtle" +// "encoding/base64" +// "encoding/json" +// "fmt" +// "github.com/pquerna/otp/totp" +// "net/http" +// "os" +// "sync/atomic" +// "time" +// ) +// +// type session struct { +// V []byte +// T time.Time +// } +// +// func (s *session) Hash() string { +// h := sha512.New() +// h.Write(s.V) +// h.Write([]byte(s.T.String())) +// return base64.URLEncoding.EncodeToString(h.Sum(nil)) +// } +// +// func newSession(w http.ResponseWriter) error { +// s := session{ +// V: make([]byte, 32), +// T: time.Now().Add(time.Hour), +// } +// n, err := rand.Read(s.V) +// if err != nil || 32 != len(s.V) || n != 32 { +// return fmt.Errorf("failed to generate session: %w", err) +// } +// http.SetCookie(w, &http.Cookie{ +// Name: "session_id", +// Value: s.Hash(), +// HttpOnly: true, +// Secure: true, +// Expires: s.T, +// SameSite: http.SameSiteStrictMode, +// }) +// sessionID.Store(&s) +// return nil +// } +// +// func validateSession(r *http.Request) bool { +// cookie, err := r.Cookie("session_id") +// if err != nil { +// return false +// } +// s := sessionID.Load() +// if s == nil || cookie.Value != s.Hash() { +// return false +// } +// if time.Now().After((*s).T) || (*s).T.After(time.Now().Add(time.Hour)) { +// sessionID.Store(nil) +// return false +// } +// return true +// } +// +// var sessionID atomic.Pointer[session] +// +// func Authenticate(next http.HandlerFunc) http.Handler { +// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// +// if validateSession(r) || loginHandler(w, r) { +// next.ServeHTTP(w, r) +// return +// } +// w.Header().Set("Content-Type", "text/html") +// w.WriteHeader(http.StatusUnauthorized) +// w.Write([]byte(html)) +// }) +// } +// +// type User struct { +// Username string // `storm:"id,index,unique"` +// OTP string +// Hash []byte +// Salt []byte +// } +// +// func randomBytes(b *[]byte, n int) error { +// *b = make([]byte, n) +// nn, err := rand.Read(*b) +// if err != nil || nn != n { +// return err +// } +// return nil +// } +// +// func (u *User) HashPassword(password string) []byte { +// h := sha512.New() +// h.Write([]byte(password)) +// h.Write(u.Salt) +// return h.Sum(nil) +// } +// +// func (u *User) CheckPassword(password string) bool { +// return subtle.ConstantTimeCompare(u.Hash, u.HashPassword(password)) == 1 +// } +// +// func Sh1(username string) string { +// h := sha1.New() +// h.Write([]byte(username)) +// return base64.URLEncoding.EncodeToString(h.Sum(nil)) +// } +// +// func LoadUser(username string) (*User, error) { +// var u User +// b, err := os.ReadFile("./.passwd/" + Sh1(username)) +// if err != nil { +// return nil, err +// } +// err = json.Unmarshal(b, &u) +// if err != nil { +// return nil, err +// } +// return &u, nil +// } +// +// func NewUser(username, password string) error { +// var u User +// u.Username = username +// +// if err := randomBytes(&u.Salt, 32); err != nil { +// return err +// } +// u.Hash = u.HashPassword(password) +// +// host, err := os.Hostname() +// if err != nil { +// host = "unknown.host" +// } +// k, err := totp.Generate(totp.GenerateOpts{ +// Issuer: host, +// AccountName: username, +// }) +// if err != nil { +// return err +// } +// u.OTP = k.Secret() +// var b []byte +// b, err = json.Marshal(&u) +// if err != nil { +// return err +// } +// fmt.Println("user", username, "created with secret", u.OTP) +// return os.WriteFile("./.passwd/"+Sh1(username), b, 0600) +// +// } +// +// func (u *User) ValidateOtp(otp string) bool { +// return totp.Validate(otp, u.OTP) +// } +// +// func loginHandler(w http.ResponseWriter, r *http.Request) bool { +// if r.Method != http.MethodPost { +// return false +// } +// +// err := r.ParseForm() +// if err != nil { +// return false +// } +// +// username := r.FormValue("username") +// password := r.FormValue("password") +// otp := r.FormValue("otp") +// +// if username == "" || password == "" || otp == "" { +// return false +// } +// +// u, err := LoadUser(username) +// if err != nil { +// return false +// } +// if !(u.CheckPassword(password) && u.ValidateOtp(otp)) { +// return false +// } +// +// return newSession(w) == nil +// } +// +// func init() { +// if err := os.MkdirAll("./.passwd", 0700); err != nil { +// panic(err) +// } +// dr, err := os.ReadDir("./.passwd") +// if err != nil { +// panic(err) +// } +// if len(dr) == 0 { +// if err = NewUser("user", "password"); err != nil { +// panic(err) +// } +// } +// } +// +// func main() { +// +// http.ListenAndServe(":8000", Authenticate(func(w http.ResponseWriter, r *http.Request) { +// w.Write([]byte("Hello, world!")) +// })) +// } +// +// var html = `<html><body><form method="post" style="width: min-content;height: min-content;transform: translate(-50%,-50%);top: 50%;position: absolute;left: 50%;"><input type="text" placeholder="username" autocomplete="off" name="username"> +// <input type="password" autocomplete="off" name="password" placeholder="password"> +// <input type="text" autocomplete="off" placeholder="otp" name="otp"> +// <input type="submit" value="submit"> +// </form></body></html>`
A htsesh/session.go

@@ -0,0 +1,132 @@

+package htsesh + +import ( + "crypto/rand" + "crypto/sha512" + "encoding/base32" + "encoding/base64" + "net/http" + "sync/atomic" + "time" +) + +type session struct { + V []byte + T time.Time +} + +func (s *session) Hash() string { + h := sha512.New() + h.Write(s.V) + h.Write([]byte(s.T.String())) + return base64.URLEncoding.EncodeToString(h.Sum(nil)) +} + +func (s *session) Validate(cookie *http.Cookie) bool { + if s == nil { + return false + } + if time.Now().After(s.T) || s.T.After(time.Now().Add(time.Hour)) { + s.V = nil + s.T = time.Time{} + sessionID.Store(nil) + return false + } + if cookie == nil || cookie.Value != s.Hash() { + return false + } + return true +} + +func sessionOK(r *http.Request) bool { + cookie, err := r.Cookie("session_id") + if err != nil { + return false + } + return sessionID.Load().Validate(cookie) +} + +func Authenticate(next http.HandlerFunc) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if sessionOK(r) { + if r.URL.Query().Has("logout") { + ji := base32.StdEncoding.EncodeToString(sessionID.Load().V[:5]) + if r.URL.Query().Get("id") == ji { + sessionID.Store(nil) + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: "", + HttpOnly: true, + Secure: true, + Expires: time.Now(), + SameSite: http.SameSiteStrictMode, + }) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) + } else { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte("<html><body><a href='?logout&id=" + ji + "'>Logout</a></body></html>")) + } + return + } + next.ServeHTTP(w, r) + } else if loginHandler(w, r) { + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) + } else { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusUnauthorized) + w.Write([]byte(FORM)) + } + }) +} + +func loginHandler(w http.ResponseWriter, r *http.Request) bool { + if r.Method != http.MethodPost || r.ParseForm() != nil { + return false + } + username := r.FormValue("username") + password := r.FormValue("password") + otp := r.FormValue("otp") + if username == "" || password == "" || otp == "" { + return false + } + if !verify(username, password, otp) { + return false + } + s := session{ + V: make([]byte, 32), + T: time.Now().Add(time.Hour), + } + n, err := rand.Read(s.V) + if err != nil || 32 != len(s.V) || n != 32 { + return false + } + sessionID.Store(&s) + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: s.Hash(), + HttpOnly: true, + Secure: true, + Expires: s.T, + SameSite: http.SameSiteStrictMode, + }) + return true +} + +var ( + verify = func(username, password, otp string) bool { + return false + } + sessionID atomic.Pointer[session] +) + +func SetVerifyFunc(f func(username, password, otp string) bool) { + verify = f +} + +var FORM = `<html><body><form method="post" style="width: min-content;height: min-content;transform: translate(-50%,-50%);top: 50%;position: absolute;left: 50%;"> +<input type="text" placeholder="username" autocomplete="off" name="username"> +<input type="password" autocomplete="off" name="password" placeholder="password"> +<input type="text" autocomplete="off" placeholder="otp" name="otp"> +<input type="submit" value="submit"> +</form></body></html>`
D http/http.go

@@ -1,218 +0,0 @@

-package http - -import ( - "crypto/rand" - "crypto/sha1" - "crypto/sha512" - "crypto/subtle" - "encoding/base64" - "encoding/json" - "fmt" - "github.com/pquerna/otp/totp" - "net/http" - "os" - "sync/atomic" - "time" -) - -type session struct { - V []byte - T time.Time -} - -func (s *session) Hash() string { - h := sha512.New() - h.Write(s.V) - h.Write([]byte(s.T.String())) - return base64.URLEncoding.EncodeToString(h.Sum(nil)) -} - -func newSession(w http.ResponseWriter) error { - s := session{ - V: make([]byte, 32), - T: time.Now().Add(time.Hour), - } - n, err := rand.Read(s.V) - if err != nil || 32 != len(s.V) || n != 32 { - return fmt.Errorf("failed to generate session: %w", err) - } - http.SetCookie(w, &http.Cookie{ - Name: "session_id", - Value: s.Hash(), - HttpOnly: true, - Secure: true, - Expires: s.T, - SameSite: http.SameSiteStrictMode, - }) - sessionID.Store(&s) - return nil -} - -func validateSession(r *http.Request) bool { - cookie, err := r.Cookie("session_id") - if err != nil { - return false - } - s := sessionID.Load() - if s == nil || cookie.Value != s.Hash() { - return false - } - if time.Now().After((*s).T) || (*s).T.After(time.Now().Add(time.Hour)) { - sessionID.Store(nil) - return false - } - return true -} - -var sessionID atomic.Pointer[session] - -func Authenticate(next http.HandlerFunc) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - if validateSession(r) || loginHandler(w, r) { - next.ServeHTTP(w, r) - return - } - w.Header().Set("Content-Type", "text/html") - w.WriteHeader(http.StatusUnauthorized) - w.Write([]byte(html)) - }) -} - -type User struct { - Username string // `storm:"id,index,unique"` - OTP string - Hash []byte - Salt []byte -} - -func randomBytes(b *[]byte, n int) error { - *b = make([]byte, n) - nn, err := rand.Read(*b) - if err != nil || nn != n { - return err - } - return nil -} - -func (u *User) HashPassword(password string) []byte { - h := sha512.New() - h.Write([]byte(password)) - h.Write(u.Salt) - return h.Sum(nil) -} - -func (u *User) CheckPassword(password string) bool { - return subtle.ConstantTimeCompare(u.Hash, u.HashPassword(password)) == 1 -} - -func Sh1(username string) string { - h := sha1.New() - h.Write([]byte(username)) - return base64.URLEncoding.EncodeToString(h.Sum(nil)) -} - -func LoadUser(username string) (*User, error) { - var u User - b, err := os.ReadFile("./.passwd/" + Sh1(username)) - if err != nil { - return nil, err - } - err = json.Unmarshal(b, &u) - if err != nil { - return nil, err - } - return &u, nil -} - -func NewUser(username, password string) error { - var u User - u.Username = username - - if err := randomBytes(&u.Salt, 32); err != nil { - return err - } - u.Hash = u.HashPassword(password) - - host, err := os.Hostname() - if err != nil { - host = "unknown.host" - } - k, err := totp.Generate(totp.GenerateOpts{ - Issuer: host, - AccountName: username, - }) - if err != nil { - return err - } - u.OTP = k.Secret() - var b []byte - b, err = json.Marshal(&u) - if err != nil { - return err - } - fmt.Println("user", username, "created with secret", u.OTP) - return os.WriteFile("./.passwd/"+Sh1(username), b, 0600) - -} - -func (u *User) ValidateOtp(otp string) bool { - return totp.Validate(otp, u.OTP) -} - -func loginHandler(w http.ResponseWriter, r *http.Request) bool { - if r.Method != http.MethodPost { - return false - } - - err := r.ParseForm() - if err != nil { - return false - } - - username := r.FormValue("username") - password := r.FormValue("password") - otp := r.FormValue("otp") - - if username == "" || password == "" || otp == "" { - return false - } - - u, err := LoadUser(username) - if err != nil { - return false - } - if !(u.CheckPassword(password) && u.ValidateOtp(otp)) { - return false - } - - return newSession(w) == nil -} - -func init() { - if err := os.MkdirAll("./.passwd", 0700); err != nil { - panic(err) - } - dr, err := os.ReadDir("./.passwd") - if err != nil { - panic(err) - } - if len(dr) == 0 { - if err = NewUser("user", "password"); err != nil { - panic(err) - } - } -} - -func main() { - - http.ListenAndServe(":8000", Authenticate(func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hello, world!")) - })) -} - -var html = `<html><body><form method="post" style="width: min-content;height: min-content;transform: translate(-50%,-50%);top: 50%;position: absolute;left: 50%;"><input type="text" placeholder="username" autocomplete="off" name="username"> -<input type="password" autocomplete="off" name="password" placeholder="password"> -<input type="text" autocomplete="off" placeholder="otp" name="otp"> - <input type="submit" value="submit"> -</form></body></html>`