all repos — auth-boilerplate @ 035e20d4a4e353e787fa5819f5efec137794db33

A simple Go web-app boilerplate.

initial commit
Marco Andronaco andronacomarco@gmail.com
Thu, 10 Oct 2024 09:09:49 +0200
commit

035e20d4a4e353e787fa5819f5efec137794db33

A .gitignore

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

+*.db +__debug_bin* +dist
A LICENSE

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

+The MIT License (MIT) + +Copyright (c) BiRabittoh + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
A gauth/gauth.go

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

+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: "/", + } +}
A go.mod

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

+module github.com/BiRabittoh/gauth + +go 1.23.2 + +require ( + github.com/glebarez/sqlite v1.11.0 + golang.org/x/crypto v0.28.0 + gorm.io/driver/sqlite v1.5.6 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/dustin/go-humanize v1.0.1 // indirect + 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/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 + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect + modernc.org/libc v1.22.5 // indirect + modernc.org/mathutil v1.5.0 // indirect + modernc.org/memory v1.5.0 // indirect + modernc.org/sqlite v1.23.1 // indirect +)
A go.sum

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

+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= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/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= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= +modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= +modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= +modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= +modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= +modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= +modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
A main.go

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

+package main + +import ( + "context" + "errors" + "fmt" + "html/template" + "log" + "net/http" + "time" + + "github.com/BiRabittoh/gauth/gauth" + "github.com/glebarez/sqlite" + "gorm.io/gorm" +) + +type key int + +type User struct { + gorm.Model + Username string + Email string + PasswordHash string + RememberToken string + ResetToken *string + ResetExpires *time.Time +} + +var ( + db *gorm.DB + 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() { + // Connessione al database SQLite + var err error + db, err = gorm.Open(sqlite.Open("database.db"), &gorm.Config{}) + if err != nil { + log.Fatal(err) + } + + // Creazione della tabella utenti + db.AutoMigrate(&User{}) + + // Gestione delle route + http.HandleFunc("GET /", loginRequired(examplePage)) + http.HandleFunc("GET /register", getRegisterHandler) + http.HandleFunc("GET /login", getLoginHandler) + http.HandleFunc("GET /reset-password", getResetPasswordHandler) + http.HandleFunc("GET /reset-password-confirm", getResetPasswordConfirmHandler) + http.HandleFunc("GET /logout", logoutHandler) + + http.HandleFunc("POST /login", postLoginHandler) + http.HandleFunc("POST /register", postRegisterHandler) + http.HandleFunc("POST /reset-password", postResetPasswordHandler) + http.HandleFunc("POST /reset-password-confirm", postResetPasswordConfirmHandler) + + port := ":8080" + log.Println("Server in ascolto su " + port) + log.Fatal(http.ListenAndServe(port, nil)) +} + +// Middleware per controllare se l'utente è loggato +// Funzione middleware per controllare se l'utente è autenticato +func loginRequired(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_token") + if err != nil { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + var user User + db.Where("remember_token = ?", cookie.Value).First(&user) + + if user.ID == 0 { + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + ctx := context.WithValue(r.Context(), userContextKey, user) + next(w, r.WithContext(ctx)) + } +} + +func getLoggedUser(r *http.Request) (User, bool) { + user, ok := r.Context().Value(userContextKey).(User) + return user, ok +} + +func examplePage(w http.ResponseWriter, r *http.Request) { + user, ok := getLoggedUser(r) + if !ok { + http.Error(w, "Utente non trovato nel contesto", http.StatusInternalServerError) + return + } + + templates.ExecuteTemplate(w, "example.html", map[string]interface{}{"User": user}) +} + +func getRegisterHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "register.html", nil) +} + +func getLoginHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "login.html", nil) +} + +func getResetPasswordHandler(w http.ResponseWriter, r *http.Request) { + templates.ExecuteTemplate(w, "reset_password.html", nil) +} + +// Gestione della registrazione +func postRegisterHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + email := r.FormValue("email") + password := r.FormValue("password") + + hashedPassword, err := g.HashPassword(password) + if err != nil { + http.Error(w, "Errore durante la registrazione", http.StatusInternalServerError) + return + } + + user := User{Username: username, Email: email, PasswordHash: hashedPassword} + db.Create(&user) + http.Redirect(w, r, "/login", http.StatusFound) + return +} + +// Gestione del login +func postLoginHandler(w http.ResponseWriter, r *http.Request) { + username := r.FormValue("username") + password := r.FormValue("password") + remember := r.FormValue("remember") + + var user User + db.Where("username = ?", username).First(&user) + + if user.ID == 0 || !g.CheckPassword(password, user.PasswordHash) { + http.Error(w, "Credenziali non valide", http.StatusUnauthorized) + return + } + + cookie, err := g.GenerateCookie(remember == "on") + if err != nil { + http.Error(w, "Errore nella generazione del token", http.StatusInternalServerError) + } + + user.RememberToken = cookie.Value + db.Save(&user) + + http.SetCookie(w, cookie) + http.Redirect(w, r, "/", http.StatusFound) + return +} + +// Logout +func logoutHandler(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, g.GenerateEmptyCookie()) + http.Redirect(w, r, "/login", http.StatusFound) +} + +// Funzione per gestire la richiesta di reset password +func postResetPasswordHandler(w http.ResponseWriter, r *http.Request) { + email := r.FormValue("email") + + var user User + db.Where("email = ?", email).First(&user) + + if user.ID == 0 { + // Non riveliamo se l'email non esiste per motivi di sicurezza + http.Redirect(w, r, "/login", http.StatusFound) + return + } + + // Genera un token di reset + resetToken, err := g.GenerateRandomToken() + if err != nil { + http.Error(w, "Errore nella generazione del token di reset", http.StatusInternalServerError) + 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) + + // 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) + log.Printf("Invio dell'email di reset per %s: %s", user.Email, resetURL) + + http.Redirect(w, r, "/login", http.StatusFound) + 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) + if err != nil { + http.Error(w, "Token non valido o scaduto", http.StatusUnauthorized) + return + } + + templates.ExecuteTemplate(w, "new_password.html", nil) +} + +func postResetPasswordConfirmHandler(w http.ResponseWriter, r *http.Request) { + user, err := validateToken(r) + if err != nil { + http.Error(w, "Token non valido o scaduto", http.StatusUnauthorized) + return + } + + password := r.FormValue("password") + + // Hash della nuova password + hashedPassword, err := g.HashPassword(password) + if err != nil { + http.Error(w, "Errore nella modifica della password", http.StatusInternalServerError) + return + } + + // 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) + + // Reindirizza alla pagina di login + http.Redirect(w, r, "/login", http.StatusFound) + return +}
A templates/example.html

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

+<!DOCTYPE html> +<html> +<head> + <title>Pagina Protetta</title> +</head> +<body> + <h1>Benvenuto, {{.User.Username}}</h1> + <p>Questa è una pagina protetta visibile solo agli utenti loggati.</p> + <a href="/logout">Logout</a> +</body> +</html>
A templates/login.html

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

+<!DOCTYPE html> +<html> +<head> + <title>Login</title> +</head> +<body> + <h1>Login</h1> + <form method="post" action="/login"> + <label>Username: <input type="text" name="username"></label><br> + <label>Password: <input type="password" name="password"></label><br> + <label>Ricordami: <input type="checkbox" name="remember"></label><br> + <input type="submit" value="Login"> + </form> + <a href="/register">Registrati</a> + <a href="/reset-password">Reset password</a> +</body> +</html>
A templates/new_password.html

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

+<!DOCTYPE html> +<html> +<head> + <title>Nuova Password</title> +</head> +<body> + <h1>Reimposta la tua password</h1> + <form method="post"> + <label>Nuova Password: <input type="password" name="password"></label><br> + <input type="submit" value="Reimposta Password"> + </form> +</body> +</html>
A templates/register.html

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

+<!DOCTYPE html> +<html> +<head> + <title>Registrazione</title> +</head> +<body> + <h1>Registrazione</h1> + <form method="post" action="/register"> + <label>Username: <input type="text" name="username"></label><br> + <label>Email: <input type="email" name="email"></label><br> + <label>Password: <input type="password" name="password"></label><br> + <input type="submit" value="Registrati"> + </form> + <a href="/login">Login</a> +</body> +</html>
A templates/reset_password.html

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

+<!DOCTYPE html> +<html> +<head> + <title>Reset Password</title> +</head> +<body> + <h1>Reset Password</h1> + <form method="post" action="/reset-password"> + <label>Email: <input type="email" name="email"></label><br> + <input type="submit" value="Reset Password"> + </form> + <a href="/login">Login</a> +</body> +</html>