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 = `
`