Password Signup & Login
This builds on Email & Password. You’ll wire up HTTP handlers for signup and login that create sessions.
Before you start
Section titled “Before you start”- Go 1.22+
- Completed code from Users & Sessions and Email & Password
- A database connection via
database/sql - Session middleware enabled so authenticated routes can read the session
- HTML pages for signup/login (server-rendered templates or your framework’s view layer)
File layout
Section titled “File layout”Build on the earlier file structure with an HTTP handlers file:
/auth /sessions.go /cookies.go /middleware.go /passwords.go /users.go/main.go/handlers /auth.go // signup/login POST handlers /pages.go // signup/login page rendering (optional)If your app keeps handlers in main.go, that’s fine too. Keep the logic the same.
Signup handler
Section titled “Signup handler”Put this in handlers/auth.go:
package handlers
import ( "database/sql" "errors" "net/http"
auth "github.com/.../auth")
func HandleSignup(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") password := r.FormValue("password")
user, err := auth.CreateUser(r.Context(), db, email, password) if err != nil { if errors.Is(err, auth.ErrEmailTaken) { http.Error(w, "Email already registered", http.StatusConflict) return } if errors.Is(err, auth.ErrInvalidEmail) || errors.Is(err, auth.ErrPasswordTooShort) || errors.Is(err, auth.ErrPasswordTooLong) || errors.Is(err, auth.ErrPasswordBreached) { http.Error(w, err.Error(), http.StatusBadRequest) return } http.Error(w, "Internal error", http.StatusInternalServerError) return }
// Create session and log them in token, err := auth.CreateSession(r.Context(), db, user.ID) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return }
auth.SetSessionCookie(w, token) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) }}After signup, create a session immediately so the user is logged in.
Login handler
Section titled “Login handler”Put this in handlers/auth.go:
func HandleLogin(db *sql.DB) http.HandlerFunc { dummyHash, err := auth.HashPassword("dummy-password") if err != nil { return func(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal error", http.StatusInternalServerError) } }
return func(w http.ResponseWriter, r *http.Request) { email := r.FormValue("email") password := r.FormValue("password")
user, err := auth.GetUserByEmail(r.Context(), db, email) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return }
hashToCompare := dummyHash if user != nil { hashToCompare = user.PasswordHash }
valid := auth.VerifyPassword(password, hashToCompare) if user == nil || !valid { http.Error(w, "Invalid email or password", http.StatusUnauthorized) return }
token, err := auth.CreateSession(r.Context(), db, user.ID) if err != nil { http.Error(w, "Internal error", http.StatusInternalServerError) return }
auth.SetSessionCookie(w, token) http.Redirect(w, r, "/dashboard", http.StatusSeeOther) }}Return the same error message whether the email doesn’t exist or the password is wrong. This prevents attackers from discovering which emails are registered.
Route wiring
Section titled “Route wiring”Put this in main.go:
import ( "database/sql" "net/http"
auth "github.com/.../auth" handlers "github.com/.../handlers")
func main() { db, _ := sql.Open("sqlite3", "app.db")
mux := http.NewServeMux() allowedOrigin := "https://example.com"
requireSameOrigin := func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !auth.VerifyRequestOrigin(r, allowedOrigin) { http.Error(w, "Forbidden", http.StatusForbidden) return } next.ServeHTTP(w, r) }) }
// Render pages mux.HandleFunc("GET /signup", handlers.HandleSignupPage) mux.HandleFunc("GET /login", handlers.HandleLoginPage)
// Process form submissions mux.Handle("POST /signup", requireSameOrigin(handlers.HandleSignup(db))) mux.Handle("POST /login", requireSameOrigin(handlers.HandleLogin(db)))
handler := auth.SessionMiddleware(db)(mux) http.ListenAndServe(":8080", handler)}This applies origin checks to signup and login POST routes.
Rendering pages
Section titled “Rendering pages”Put this in handlers/pages.go:
package handlers
import "net/http"
func HandleSignupPage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`<!doctype html><html> <body> <h1>Sign up</h1> <form method="post" action="/signup"> <label>Email <input name="email" type="email" required /></label><br /> <label>Password <input name="password" type="password" required /></label><br /> <button type="submit">Create account</button> </form> <p><a href="/login">Already have an account? Log in</a></p> </body></html>`))}
func HandleLoginPage(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`<!doctype html><html> <body> <h1>Log in</h1> <form method="post" action="/login"> <label>Email <input name="email" type="email" required /></label><br /> <label>Password <input name="password" type="password" required /></label><br /> <button type="submit">Log in</button> </form> <p><a href="/signup">Need an account? Sign up</a></p> </body></html>`))}Use standard HTML form fields:
<input name="email" type="email" required /><input name="password" type="password" required />Flow checklist:
- Read form values (
email,password) - Create or look up user
- Verify password (always compare against a hash on login)
- Create session
- Set session cookie and redirect
Next up
Section titled “Next up”Head to Verification & Recovery to add email verification and password reset.