added some http auth functions
sophuwu sophie@skisiel.com
Sun, 18 May 2025 15:55:17 +0200
1 files changed,
218 insertions(+),
0 deletions(-)
jump to
A
http/http.go
@@ -0,0 +1,218 @@
+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>`