all repos — auth-boilerplate @ b30162a9cd5be5a02eeb8a33bbe54b02dd03f8a3

A simple Go web-app boilerplate.

add keystore, load pepper from .env
Marco Andronaco andronacomarco@gmail.com
Thu, 10 Oct 2024 10:00:24 +0200
commit

b30162a9cd5be5a02eeb8a33bbe54b02dd03f8a3

parent

035e20d4a4e353e787fa5819f5efec137794db33

7 files changed, 113 insertions(+), 114 deletions(-)

jump to
A .env.example

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

+APP_PEPPER=AnyRandomString
M .gitignore.gitignore

@@ -1,3 +1,4 @@

+.env *.db __debug_bin* dist
A auth/auth.go

@@ -0,0 +1,67 @@

+package auth + +import ( + "crypto/rand" + "encoding/hex" + "net/http" + "time" + + "golang.org/x/crypto/bcrypt" +) + +type Auth struct { + Pepper string +} + +func NewAuth(pepper string) *Auth { + return &Auth{ + Pepper: pepper, + } +} + +func (g Auth) HashPassword(password string) (string, error) { + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password+g.Pepper), bcrypt.DefaultCost) + if err != nil { + return "", err + } + return string(hashedPassword), nil +} + +func (g Auth) CheckPassword(password, hash string) bool { + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+g.Pepper)) + return err == nil +} + +func (g Auth) GenerateRandomToken() (string, error) { + token := make([]byte, 32) + _, err := rand.Read(token) + if err != nil { + return "", err + } + return hex.EncodeToString(token), nil +} + +func (g Auth) GenerateCookie(duration time.Duration) (*http.Cookie, error) { + sessionToken, err := g.GenerateRandomToken() + if err != nil { + return nil, err + } + + return &http.Cookie{ + Name: "session_token", + Value: sessionToken, + Expires: time.Now().Add(duration), + Path: "/", + HttpOnly: true, + Secure: true, + }, nil +} + +func (g Auth) GenerateEmptyCookie() *http.Cookie { + return &http.Cookie{ + Name: "session_token", + Value: "", + Expires: time.Now().Add(-1 * time.Hour), + Path: "/", + } +}
D gauth/gauth.go

@@ -1,78 +0,0 @@

-package gauth - -import ( - "crypto/rand" - "encoding/hex" - "net/http" - "time" - - "golang.org/x/crypto/bcrypt" -) - -type Gauth struct { - Pepper string - SessionTokenDuration time.Duration - LongSessionTokenDuration time.Duration -} - -func NewGauth(pepper string, sessionTokenDuration, longSessionTokenDuration time.Duration) *Gauth { - return &Gauth{ - Pepper: pepper, - SessionTokenDuration: sessionTokenDuration, - LongSessionTokenDuration: longSessionTokenDuration, - } -} - -func (g Gauth) HashPassword(password string) (string, error) { - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password+g.Pepper), bcrypt.DefaultCost) - if err != nil { - return "", err - } - return string(hashedPassword), nil -} - -func (g Gauth) CheckPassword(password, hash string) bool { - err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+g.Pepper)) - return err == nil -} - -func (g Gauth) GenerateRandomToken() (string, error) { - token := make([]byte, 32) - _, err := rand.Read(token) - if err != nil { - return "", err - } - return hex.EncodeToString(token), nil -} - -func (g Gauth) GenerateCookie(long bool) (*http.Cookie, error) { - sessionToken, err := g.GenerateRandomToken() - if err != nil { - return nil, err - } - - var expiration time.Duration - if long { - expiration = g.LongSessionTokenDuration - } else { - expiration = g.SessionTokenDuration - } - - return &http.Cookie{ - Name: "session_token", - Value: sessionToken, - Expires: time.Now().Add(expiration), - Path: "/", - HttpOnly: true, - Secure: true, - }, nil -} - -func (g Gauth) GenerateEmptyCookie() *http.Cookie { - return &http.Cookie{ - Name: "session_token", - Value: "", - Expires: time.Now().Add(-1 * time.Hour), - Path: "/", - } -}
M go.modgo.mod

@@ -1,8 +1,9 @@

-module github.com/BiRabittoh/gauth +module github.com/birabittoh/auth-boilerplate go 1.23.2 require ( + github.com/birabittoh/myks v0.0.2 github.com/glebarez/sqlite v1.11.0 golang.org/x/crypto v0.28.0 gorm.io/driver/sqlite v1.5.6

@@ -15,6 +16,7 @@ github.com/glebarez/go-sqlite v1.21.2 // indirect

github.com/google/uuid v1.3.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
M go.sumgo.sum

@@ -1,3 +1,7 @@

+github.com/BiRabittoh/myks v0.0.0-20241010070553-ecfcd30b3ca1 h1:Dd9XkQaIIaIS6eTirtJlz0QWzo6Eg4yBt75iR+6Fobk= +github.com/BiRabittoh/myks v0.0.0-20241010070553-ecfcd30b3ca1/go.mod h1:jIJU6HMDJHJZL3LYR/mgOUeA5RTDEhqJNF+HcavHlsM= +github.com/birabittoh/myks v0.0.2 h1:EBukMUsAflwiqdNo4LE7o2WQdEvawty5ewCZWY+IXSU= +github.com/birabittoh/myks v0.0.2/go.mod h1:klNWaeUWm7TmhnBHBMt9vALwCHW11/Xw1BpCNkCx7hs= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=

@@ -10,6 +14,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=

github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
M main.gomain.go

@@ -2,15 +2,17 @@ package main

import ( "context" - "errors" "fmt" "html/template" "log" "net/http" + "os" "time" - "github.com/BiRabittoh/gauth/gauth" + "github.com/birabittoh/auth-boilerplate/auth" + "github.com/birabittoh/myks" "github.com/glebarez/sqlite" + "github.com/joho/godotenv" "gorm.io/gorm" )

@@ -22,23 +24,27 @@ Username string

Email string PasswordHash string RememberToken string - ResetToken *string - ResetExpires *time.Time } var ( - db *gorm.DB + db *gorm.DB + g *auth.Auth + + ks = myks.New[uint](0) durationDay = 24 * time.Hour durationWeek = 7 * durationDay - g = gauth.NewGauth("superSecretPepper", durationDay, durationWeek) templates = template.Must(template.ParseGlob("templates/*.html")) ) const userContextKey key = 0 func main() { + err := godotenv.Load() + if err != nil { + log.Println("Error loading .env file") + } + // Connessione al database SQLite - var err error db, err = gorm.Open(sqlite.Open("database.db"), &gorm.Config{}) if err != nil { log.Fatal(err)

@@ -46,6 +52,9 @@ }

// Creazione della tabella utenti db.AutoMigrate(&User{}) + + // Inizializzazione di gauth + g = auth.NewAuth(os.Getenv("APP_PEPPER")) // Gestione delle route http.HandleFunc("GET /", loginRequired(examplePage))

@@ -147,13 +156,20 @@ http.Error(w, "Credenziali non valide", http.StatusUnauthorized)

return } - cookie, err := g.GenerateCookie(remember == "on") + var duration time.Duration + if remember == "on" { + duration = durationWeek + } else { + duration = durationDay + } + + cookie, err := g.GenerateCookie(duration) if err != nil { http.Error(w, "Errore nella generazione del token", http.StatusInternalServerError) } - user.RememberToken = cookie.Value - db.Save(&user) + // user.RememberToken = cookie.Value + // db.Save(&user) http.SetCookie(w, cookie) http.Redirect(w, r, "/", http.StatusFound)

@@ -187,10 +203,7 @@ return

} // Imposta una scadenza di 1 ora per il reset token - expiration := time.Now().Add(1 * time.Hour) - user.ResetToken = &resetToken - user.ResetExpires = &expiration - db.Save(&user) + ks.Set(resetToken, user.ID, time.Hour) // Simula l'invio di un'email con il link di reset (in questo caso viene stampato sul log) resetURL := fmt.Sprintf("http://localhost:8080/reset-password-confirm?token=%s", resetToken)

@@ -201,26 +214,10 @@ return

} -func validateToken(r *http.Request) (user User, err error) { - token := r.URL.Query().Get("token") - - db.Where("reset_token = ?", token).First(&user) - - // Verifica se il token รจ valido e non scaduto - if user.ResetExpires == nil { - err = errors.New("nil value") - return - } - - if user.ID == 0 || time.Now().After(*user.ResetExpires) { - err = errors.New("not found") - } - return -} - // Funzione per confermare il reset della password (usando il token) func getResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { - _, err := validateToken(r) + token := r.URL.Query().Get("token") + _, err := ks.Get(token) if err != nil { http.Error(w, "Token non valido o scaduto", http.StatusUnauthorized) return

@@ -230,11 +227,15 @@ templates.ExecuteTemplate(w, "new_password.html", nil)

} func postResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { - user, err := validateToken(r) + token := r.URL.Query().Get("token") + userID, err := ks.Get(token) if err != nil { http.Error(w, "Token non valido o scaduto", http.StatusUnauthorized) return } + + var user User + db.First(&user, *userID) password := r.FormValue("password")

@@ -247,9 +248,8 @@ }

// Aggiorna l'utente con la nuova password e rimuove il token di reset user.PasswordHash = hashedPassword - user.ResetToken = nil - user.ResetExpires = nil db.Save(&user) + ks.Delete(token) // Reindirizza alla pagina di login http.Redirect(w, r, "/login", http.StatusFound)