Skip to content

Password Signup & Login

View as Markdown

This builds on Email & Password. You’ll wire up HTTP handlers for signup and login that create sessions.

  • 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)

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.

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.

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.

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.

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:

  1. Read form values (email, password)
  2. Create or look up user
  3. Verify password (always compare against a hash on login)
  4. Create session
  5. Set session cookie and redirect

Head to Verification & Recovery to add email verification and password reset.