git.sophuwu.com > goauth   
              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>`