sanitize email, username
Marco Andronaco andronacomarco@gmail.com
Thu, 10 Oct 2024 21:07:03 +0200
11 files changed,
128 insertions(+),
34 deletions(-)
M
Dockerfile
→
Dockerfile
@@ -26,6 +26,7 @@ FROM scratch AS build-release-stage
WORKDIR /app +COPY static ./static COPY templates ./templates COPY --from=builder /dist .
M
docker-compose.yaml
→
docker-compose.yaml
@@ -6,9 +6,11 @@ build: .
image: ghcr.io/birabittoh/auth-boilerplate:main container_name: app restart: unless-stopped + env_file: .env ports: - 3000:3000 - volumes: - - /etc/localtime:/etc/localtime:ro - - ./templates:/app/templates - - ./data:/app/data+# volumes: +# - /etc/localtime:/etc/localtime:ro +# - ./templates:/app/templates +# - ./static:/app/static +# - ./data:/app/data
M
src/app/functions.go
→
src/app/functions.go
@@ -7,10 +7,42 @@ "fmt"
"log" "net/http" "os" + "regexp" + "strings" "time" "github.com/birabittoh/auth-boilerplate/src/email" ) + +const ( + minUsernameLength = 3 + maxUsernameLength = 10 +) + +var ( + validUsername = regexp.MustCompile(`^[a-z0-9._-]+$`) + validEmail = regexp.MustCompile(`^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$`) +) + +func sanitizeUsername(username string) (string, error) { + username = strings.ToLower(username) + + if !validUsername.MatchString(username) || len(username) < minUsernameLength || len(username) > maxUsernameLength { + return "", errors.New("invalid username") + } + + return username, nil +} + +func sanitizeEmail(email string) (string, error) { + email = strings.ToLower(email) + + if !validEmail.MatchString(email) { + return "", fmt.Errorf("invalid email") + } + + return email, nil +} func login(w http.ResponseWriter, userID uint, remember bool) { var duration time.Duration
M
src/app/handlers.go
→
src/app/handlers.go
@@ -38,13 +38,21 @@ http.Error(w, "Registration is currently disabled.", http.StatusForbidden)
return } - username := r.FormValue("username") - email := r.FormValue("email") - password := r.FormValue("password") + username, err := sanitizeUsername(r.FormValue("username")) + if err != nil { + http.Error(w, "Invalid username.", http.StatusBadRequest) + return + } - hashedPassword, salt, err := g.HashPassword(password) + email, err := sanitizeEmail(r.FormValue("email")) if err != nil { - http.Error(w, "Could not hash your password.", http.StatusInternalServerError) + http.Error(w, "Invalid email.", http.StatusBadRequest) + return + } + + hashedPassword, salt, err := g.HashPassword(r.FormValue("password")) + if err != nil { + http.Error(w, "Invalid password.", http.StatusBadRequest) return }@@ -63,7 +71,6 @@ }
login(w, user.ID, false) http.Redirect(w, r, "/login", http.StatusFound) - return } func postLoginHandler(w http.ResponseWriter, r *http.Request) {@@ -81,7 +88,6 @@ }
login(w, user.ID, remember == "on") http.Redirect(w, r, "/", http.StatusFound) - return } func logoutHandler(w http.ResponseWriter, r *http.Request) {@@ -110,7 +116,6 @@ ks.Set(resetToken, user.ID, time.Hour)
sendResetEmail(user.Email, resetToken) http.Redirect(w, r, "/login", http.StatusFound) - return }@@ -140,7 +145,7 @@ password := r.FormValue("password")
hashedPassword, salt, err := g.HashPassword(password) if err != nil { - http.Error(w, "Could not edit your password.", http.StatusInternalServerError) + http.Error(w, "Invalid password.", http.StatusBadRequest) return }@@ -150,5 +155,4 @@ db.Save(&user)
ks.Delete(token) http.Redirect(w, r, "/login", http.StatusFound) - return }
M
src/app/init.go
→
src/app/init.go
@@ -71,8 +71,11 @@ registrationEnabled = false
} // Init auth and email - g = auth.NewAuth(os.Getenv("APP_PEPPER")) m = loadEmailConfig() + g = auth.NewAuth(os.Getenv("APP_PEPPER"), auth.DefaultMaxPasswordLength) + if g == nil { + log.Fatal("Could not init authentication.") + } os.MkdirAll(dataDir, os.ModePerm) dbPath := filepath.Join(dataDir, dbName) + "?_pragma=foreign_keys(1)"
M
src/auth/auth.go
→
src/auth/auth.go
@@ -3,38 +3,66 @@
import ( "crypto/rand" "encoding/hex" + "errors" "net/http" "time" + "unicode" "golang.org/x/crypto/bcrypt" ) type Auth struct { - Pepper string + Pepper string + spicesLength int } -func NewAuth(pepper string) *Auth { +const ( + minPasswordLength = 6 + maxHashLength = 72 + DefaultMaxPasswordLength = 56 // leaves 16 bytes for salt and pepper +) + +func NewAuth(pepper string, maxPasswordLength uint) *Auth { + if maxPasswordLength > 70 { + return nil + } + + hash, err := bcrypt.GenerateFromPassword([]byte(pepper), bcrypt.DefaultCost) + if err != nil { + return nil + } + + spicesLength := int(maxHashLength-maxPasswordLength) / 2 return &Auth{ - Pepper: pepper, + Pepper: hex.EncodeToString(hash)[:spicesLength], + spicesLength: spicesLength, } } func (g Auth) HashPassword(password string) (hashedPassword, salt string, err error) { - salt, err = g.GenerateRandomToken(16) + if !isASCII(password) || len(password) < minPasswordLength { + err = errors.New("invalid password") + return + } + + salt, err = g.GenerateRandomToken(g.spicesLength) if err != nil { return } + salt = salt[:g.spicesLength] bytesPassword, err := bcrypt.GenerateFromPassword([]byte(password+salt+g.Pepper), bcrypt.DefaultCost) if err != nil { return } + hashedPassword = string(bytesPassword) return } func (g Auth) CheckPassword(password, salt, hash string) bool { - return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+salt+g.Pepper)) == nil + err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password+salt+g.Pepper)) + return err == nil } func (g Auth) GenerateRandomToken(n int) (string, error) {@@ -70,3 +98,12 @@ Expires: time.Now().Add(-1 * time.Hour),
Path: "/", } } + +func isASCII(s string) bool { + for i := 0; i < len(s); i++ { + if s[i] > unicode.MaxASCII { + return false + } + } + return true +}
M
templates/example.html
→
templates/example.html
@@ -10,7 +10,7 @@ <link rel="stylesheet" href="/static/style.css">
</head> <body> - <h1>Welcome, {{.User.Username}}!</h1> + <h1>Welcome, <i>{{.User.Username}}</i>!</h1> <p>This page is only accessible by users who have logged in.</p> <a href="/logout">Logout</a> </body>
M
templates/login.html
→
templates/login.html
@@ -14,11 +14,11 @@ <h1>Login</h1>
<form method="post" action="/login"> <label> <span>Username:</span> - <input type="text" name="username" autocomplete="off" required> + <input type="text" name="username" autocomplete="off" placeholder="Username" required> </label> <label> <span>Password:</span> - <input type="password" name="password" required> + <input type="password" name="password" placeholder="Password" required> </label> <label> <span>Remember me:</span>
M
templates/new_password.html
→
templates/new_password.html
@@ -4,16 +4,19 @@
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>Pagina Protetta</title> + <title>Reset password</title> <link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"> <link rel="stylesheet" href="/static/style.css"> </head> <body> - <h1>Reimposta la tua password</h1> + <h1>Reset password</h1> <form method="post"> - <label>Nuova Password: <input type="password" name="password" required></label><br> - <input type="submit" value="Reimposta Password"> + <label> + <span>New password:</span> + <input type="password" name="password" placeholder="Max 56 chars, ASCII only" required> + </label> + <input type="submit" value="Reset password"> </form> </body>
M
templates/register.html
→
templates/register.html
@@ -12,10 +12,19 @@
<body> <h1>Sign up</h1> <form method="post" action="/register"> - <label>Username: <input type="text" name="username" required></label><br> - <label>Email: <input type="email" name="email" required></label><br> - <label>Password: <input type="password" name="password" required></label><br> - <input type="submit" value="Registrati"> + <label> + <span>Username:</span> + <input type="text" name="username" placeholder="^[a-z0-9._-]+$" required> + </label> + <label> + <span>Email:</span> + <input type="email" name="email" placeholder="Email" required> + </label> + <label> + <span>Password:</span> + <input type="password" name="password" placeholder="Max 56 chars, ASCII only" required> + </label> + <input type="submit" value="Sign up"> </form> <a href="/login">Login</a> <a href="/reset-password">Reset password</a>
M
templates/reset_password.html
→
templates/reset_password.html
@@ -10,10 +10,13 @@ <link rel="stylesheet" href="/static/style.css">
</head> <body> - <h1>Reset Password</h1> + <h1>Reset password</h1> <form method="post" action="/reset-password"> - <label>Email: <input type="email" name="email" required></label><br> - <input type="submit" value="Reset Password"> + <label> + <span>Email:</span> + <input type="email" name="email" placeholder="Email" required> + </label> + <input type="submit" value="Reset password"> </form> <a href="/register">Sign up</a> <a href="/login">Login</a>