git.sophuwu.com > authuwu
added http handlers
sophuwu sophie@sophuwu.com
Wed, 16 Jul 2025 20:52:39 +0200
commit

acc2ef94bd9e7c87b754ac75964a22cc3876bf2d

parent

05670058fc1ff89252c6767c0f223dece37a9dd6

5 files changed, 184 insertions(+), 2 deletions(-)

jump to
M authuwu.goauthuwu.go

@@ -1,1 +1,98 @@

package authuwu + +import ( + "fmt" + "git.sophuwu.com/authuwu/cookie" + uwudb "git.sophuwu.com/authuwu/db" + "git.sophuwu.com/authuwu/userpass" + "net/http" + "time" +) + +func OpenDB(path string) error { + return uwudb.Open(path) +} +func CloseDB() error { + return uwudb.Close() +} + +func NewAuthuwuHandler(h http.Handler, cookieTime time.Duration) *AuthuwuHandler { + return &AuthuwuHandler{ + Handler: h, + CookieTime: cookieTime, + } +} + +type AuthuwuHandler struct { + Handler http.Handler + CookieTime time.Duration +} + +// LoginPage is the HTML template for the login page. Should contain a form that submits to the login handler. +// Must action to "?authuwu:login" with method POST. +// Must contain inputs for username, password +var LoginPage = `<html> +<head> +<title>Login</title> +</head> +<body> +<h1>Login</h1> +<form method="POST" action="?authuwu:login"> +<label for="username">Username:</label> +<input type="text" id="username" name="username" required> +<label for="password">Password:</label> +<input type="password" id="password" name="password" required> +<input type="submit" value="Login"> +</form> +</body> +</html> +` + +func (h *AuthuwuHandler) loginHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + fmt.Fprint(w, LoginPage) + w.WriteHeader(http.StatusOK) + return + } + username := r.FormValue("username") + password := r.FormValue("password") + if username == "" || password == "" { + http.Redirect(w, r, r.URL.Path+"?authuwu:login", http.StatusBadRequest) + return + } + ok, err := userpass.UserAuth(username, password) + if err != nil || !ok { + http.Redirect(w, r, r.URL.Path+"?authuwu:login", http.StatusUnauthorized) + return + } + c, err := cookie.NewCookie(username, h.CookieTime) + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + http.SetCookie(w, &c) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) +} + +func (h *AuthuwuHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.RawQuery == "authuwu:login" { + h.loginHandler(w, r) + return + } + ok, user, err := cookie.GetCookie(r) + if err == nil && ok && user != "" { + var u *userpass.User + u, err = userpass.GetUser(user) + if err == nil && u != nil && u.Username != "" { + if r.URL.RawQuery == "authuwu:logout" { + _ = cookie.DeleteCookie(r) + http.Redirect(w, r, r.URL.Path, http.StatusSeeOther) + return + } + h.Handler.ServeHTTP(w, r) + return + } + } + h.loginHandler(w, r) +}
A cookie/cookie.go

@@ -0,0 +1,82 @@

+package cookie + +import ( + "git.sophuwu.com/authuwu/db" + "git.sophuwu.com/authuwu/standard" + "net/http" + "time" +) + +type Cookie struct { + Secret []byte `storm:"id,unique,index"` + User string `storm:"index"` + Expires time.Time `storm:"index"` +} + +func (c *Cookie) Encode() string { + return standard.NewEncoder().EncodeToString(c.Secret) +} + +func random(len int) []byte { + b := make([]byte, len) + return b +} + +func NewCookie(user string, expires time.Duration) (http.Cookie, error) { + b := random(standard.CookieLength) + var c Cookie + c.Secret = b + c.User = user + c.Expires = time.Now().Add(expires) + err := db.AuthUwu.Save(&c) + if err != nil { + return http.Cookie{}, err + } + return http.Cookie{ + Name: "authuwu_session", + Value: c.Encode(), + Expires: c.Expires, + HttpOnly: true, + Secure: true, + SameSite: http.SameSiteStrictMode, + }, err +} + +func CheckCookie(secret string) (bool, string, error) { + var c Cookie + b, err := standard.NewEncoder().DecodeString(secret) + if err != nil || len(b) != standard.CookieLength { + return false, "", err + } + err = db.AuthUwu.One("Secret", b, &c) + if err != nil { + return false, "", err + } + if time.Now().After(c.Expires) { + err = db.AuthUwu.DeleteStruct(&c) + return false, "", err + } + return true, c.User, nil +} + +func GetCookie(r *http.Request) (bool, string, error) { + c, err := r.Cookie("authuwu_session") + if err != nil || c == nil { + return false, "", err + } + return CheckCookie(c.Value) +} + +func DeleteCookie(r *http.Request) error { + c, err := r.Cookie("authuwu_session") + if err != nil || c == nil { + return err + } + b, err := standard.NewEncoder().DecodeString(c.Value) + if err != nil || len(b) != standard.CookieLength { + return err + } + var cookie Cookie + cookie.Secret = b + return db.AuthUwu.DeleteStruct(&cookie) +}
M otp/otp.gootp/otp.go

@@ -21,6 +21,6 @@ return totp.Validate(otp, u.OTP)

} type User struct { - Username string `storm:"id"` + Username string `storm:"id,unique,index"` OTP string }
M standard/standard.gostandard/standard.go

@@ -15,6 +15,9 @@

// Encoding is the encoding used for storing passwords. const Encoding = "base64URL" +// CookieLength is the length of the cookie secret in bytes. +const CookieLength = 72 + func NewHash() hash.Hash { return sha512.New384() }
M userpass/userpass.gouserpass/userpass.go

@@ -59,7 +59,7 @@ return u.Password.CheckPassword(password)

} type User struct { - Username string `storm:"id"` + Username string `storm:"id,unique,index"` Password Password `storm:"inline"` }