---
title: Password Signup & Login
description: Wire email/password into HTTP handlers and sessions.
---

import LLMExport from '../../components/LLMExport.astro';

<LLMExport title="Password Signup & Login" slug="password-signup-login" />

This builds on [Email & Password](/email-password). You'll wire up HTTP handlers for signup and login that create sessions.

## Before you start

- Go 1.22+
- Completed code from [Users & Sessions](/users-sessions) and [Email & Password](/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

Build on the earlier file structure with an HTTP handlers file:

```diff
 /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

Put this in `handlers/auth.go`:
```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

Put this in `handlers/auth.go`:
```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

Put this in `main.go`:
```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

Put this in `handlers/pages.go`:

```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:

```html
<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

## Next up

Head to [Verification & Recovery](/verification-recovery) to add email verification and password reset.
