# Lesson 8 — Auth Middleware & Route Protection > **New Go concepts in this lesson:** `context.Context` in depth > (`context.WithValue`, `r.WithContext`), private types used purely as > unique context keys, type assertions applied for real. This is the > concept-heaviest lesson in the course — take it slowly, and don't skip > Part A. ## Why we need this Right now, `Me` manually checks the session and returns 401 if there's no user. As we add more protected routes later, copy-pasting that check into every handler is error-prone: forget it once, and you've got an unprotected route. The fix is **middleware that guards routes**, plus using the request's **context** to hand the logged-in user down to whichever handler runs next. ## Part A — standalone playground This lesson is really about one core Go mechanism: `context.Context` as a way to pass request-scoped values through a middleware chain. Let's build it from scratch, no chi, no scs — just `net/http` and `context`. ```bash mkdir ~/go-playground/context-demo && cd ~/go-playground/context-demo go mod init context-demo ``` **`main.go`** ```go package main import ( "context" "fmt" "log" "net/http" ) // 1. A custom type for our context key. Using a plain string like "user" // as a key is risky - other packages might use the same string and // silently collide. A private, unexported type guarantees uniqueness. type contextKey string const userContextKey contextKey = "user" type User struct { ID int Email string } // 2. Middleware that pretends to authenticate a request (checks a fake // header instead of a real session, just to isolate the context concept). func fakeAuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if token != "secret-token" { http.Error(w, "unauthorized", http.StatusUnauthorized) return // IMPORTANT: we do NOT call next.ServeHTTP - chain stops here } user := &User{ID: 1, Email: "hamid@example.com"} // 3. Store the user in a NEW context, derived from the request's // existing context, then build a NEW request carrying that context. ctx := context.WithValue(r.Context(), userContextKey, user) r = r.WithContext(ctx) // 4. Pass the request onward - now carrying the user. next.ServeHTTP(w, r) }) } func protectedHandler(w http.ResponseWriter, r *http.Request) { // 5. Read the value back out of the context, in the final handler. user, ok := r.Context().Value(userContextKey).(*User) if !ok { // Should never happen if the middleware ran correctly, but // defensive code is cheap insurance. http.Error(w, "no user in context", http.StatusInternalServerError) return } fmt.Fprintf(w, "hello, %s (id=%d)\n", user.Email, user.ID) } func main() { mux := http.NewServeMux() mux.Handle("/protected", fakeAuthMiddleware(http.HandlerFunc(protectedHandler))) log.Println("listening on :4000") log.Fatal(http.ListenAndServe(":4000", mux)) } ``` Run it: ```bash go run . ``` Test it: ```bash curl http://localhost:4000/protected # 401 unauthorized curl -H "Authorization: secret-token" http://localhost:4000/protected # hello, hamid@example.com (id=1) ``` Line by line — this is the trickiest idiom in the whole course, worth sitting with: - `type contextKey string` then `const userContextKey contextKey = "user"` — why not just use the plain string `"user"` directly? Because `context.WithValue` keys are compared by **both type and value**. If two unrelated packages both used the plain string `"user"` as a key, they'd accidentally read/overwrite each other's data. By defining our own named type `contextKey`, our key `userContextKey` can never collide with a plain `string` key or another package's own custom-typed key — even if the underlying text is identical. This is a well-known, idiomatic Go pattern specifically to avoid that collision class. - `if token != "secret-token" { http.Error(...); return }` — note there's **no call to `next.ServeHTTP`** in this branch. This is the entire mechanism of "blocking" a request in middleware: simply *don't* call the next handler. The chain just stops, and whatever you already wrote to `w` (here, the 401) is the final response. - `context.WithValue(r.Context(), userContextKey, user)` — contexts are **immutable**. You can't add a value to an existing context; `WithValue` returns a **brand-new** context wrapping the old one plus the new key/value pair. The original `r.Context()` is untouched. - `r = r.WithContext(ctx)` — similarly, `*http.Request` is designed so you don't mutate its context in place; `WithContext` returns a **new** `*http.Request` (a shallow copy) with the new context attached. Reassigning `r` to this new value is how we "carry" the updated context forward. - `next.ServeHTTP(w, r)` — passing the **new** `r` (with the user embedded) onward. Anything called after this point — more middleware, or the final handler — can pull the user back out. - `r.Context().Value(userContextKey).(*User)` — `Value` returns `any` (could be anything, or `nil` if the key isn't present), so we need a **type assertion** (`.(*User)`) to convert it back to our concrete type. The two-value form `user, ok := ...` is the safe version: `ok` is `false` if the assertion fails (wrong type, or key missing) instead of panicking. **Always use the two-value form** when the value's presence isn't 100% guaranteed — a single-value assertion panics on failure, crashing your whole request. Try removing the `Authorization` header check entirely and calling `protectedHandler` directly, without going through the middleware — you'll see the `ok` false-path trigger, since nothing populated the context. ## Part B — apply it to the project We'll build real middleware that checks the actual session (Lesson 6), loads the actual user from MySQL (Lesson 4), and stores it in context using the exact pattern from Part A. **`internal/middleware/require_auth.go`** ```go package middleware import ( "context" "log/slog" "net/http" "github.com/alexedwards/scs/v2" "git.hamidsoltani.com/hamid/go-simple-api/internal/models" "git.hamidsoltani.com/hamid/go-simple-api/internal/session" ) type contextKey string const userContextKey contextKey = "current_user" // RequireAuth is a middleware FACTORY - same three-layer shape as // RequestLogger from Lesson 2. It takes the dependencies it needs // (sessions, userRepo, logger), and returns the actual chi middleware. func RequireAuth(sessions *scs.SessionManager, userRepo *models.UserRepository, logger *slog.Logger) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID := sessions.GetInt(r.Context(), session.UserIDKey) if userID == 0 { writeUnauthorized(w) return } user, err := userRepo.FindByID(r.Context(), userID) if err != nil { // Covers both "not found" (e.g. account deleted after // login) and real DB errors - either way, this request // cannot proceed as authenticated. logger.Error("require auth: find user failed", "error", err, "user_id", userID) writeUnauthorized(w) return } ctx := context.WithValue(r.Context(), userContextKey, user) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // CurrentUser is how handlers pull the authenticated user back out. // Handlers never touch userContextKey directly - they just call this. func CurrentUser(r *http.Request) *models.User { user, ok := r.Context().Value(userContextKey).(*models.User) if !ok { return nil } return user } func writeUnauthorized(w http.ResponseWriter) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusUnauthorized) w.Write([]byte(`{"error":"unauthorized"}`)) } ``` This should now read very familiarly — it's Part A's pattern with the fake pieces swapped for real ones: - `sessions.GetInt(...)` — same check `Me` did manually in Lesson 6. - `userRepo.FindByID(...)` — same repository lookup `Me` did. - `context.WithValue` / `r.WithContext` / `next.ServeHTTP(w, r.WithContext(ctx))` — identical mechanism from Part A. - `CurrentUser(r *http.Request) *models.User` — a small **exported helper function**, not a method, wrapping the type assertion so handlers never need to know about `userContextKey` at all (it's unexported — package-private — precisely so only this file can create or read that specific key). This pairs a private context key with a public accessor function, a common Go idiom. - `writeUnauthorized` — a tiny local helper, written by hand instead of reusing `handlers.writeError`, because `internal/middleware` and `internal/handlers` are separate packages, and `writeError` is unexported in `handlers`. This is an intentional package boundary, not an oversight — if we wanted to share it, we'd need to export it (`WriteError`) from a package both can import. **Simplify `Me` in `internal/handlers/auth.go`** now that middleware does the lookup: ```go func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) { user := middleware.CurrentUser(r) if user == nil { writeError(w, http.StatusUnauthorized, "not logged in") return } writeJSON(w, http.StatusOK, map[string]any{ "id": user.ID, "email": user.Email, }) } ``` Add the import: `"git.hamidsoltani.com/hamid/go-simple-api/internal/middleware"`. `Me` no longer touches `h.sessions` or does a `FindByID` itself at all — the middleware already did that work before `Me` ever runs, and just hands us the result via `CurrentUser(r)`. The `nil` check stays as a defensive safety net (in case someone wires this handler up without the middleware by mistake), but in normal operation it should never trigger. **Update `internal/router/router.go`** to apply `RequireAuth` to `/me`, using chi's route grouping: ```go package router import ( "database/sql" "log/slog" "time" "github.com/alexedwards/scs/v2" "github.com/go-chi/chi/v5" chimw "github.com/go-chi/chi/v5/middleware" "git.hamidsoltani.com/hamid/go-simple-api/internal/config" "git.hamidsoltani.com/hamid/go-simple-api/internal/handlers" "git.hamidsoltani.com/hamid/go-simple-api/internal/middleware" "git.hamidsoltani.com/hamid/go-simple-api/internal/models" "git.hamidsoltani.com/hamid/go-simple-api/internal/oauth" ) func New(logger *slog.Logger, db *sql.DB, sessions *scs.SessionManager, cfg config.Config) *chi.Mux { r := chi.NewRouter() r.Use(chimw.RequestID) r.Use(middleware.RequestLogger(logger)) r.Use(chimw.Recoverer) r.Use(chimw.Timeout(60 * time.Second)) r.Use(sessions.LoadAndSave) r.Get("/health", handlers.Health) userRepo := models.NewUserRepository(db) authHandler := handlers.NewAuthHandler(userRepo, sessions, logger) requireAuth := middleware.RequireAuth(sessions, userRepo, logger) r.Post("/register", authHandler.Register) r.Post("/login", authHandler.Login) r.Post("/logout", authHandler.Logout) // Group: every route inside here goes through requireAuth first. r.Group(func(r chi.Router) { r.Use(requireAuth) r.Get("/me", authHandler.Me) }) googleConfig := oauth.NewGoogleConfig(cfg) googleHandler := handlers.NewGoogleOAuthHandler(googleConfig, userRepo, sessions, logger) r.Get("/auth/google/login", googleHandler.Login) r.Get("/auth/google/callback", googleHandler.Callback) return r } ``` - `requireAuth := middleware.RequireAuth(sessions, userRepo, logger)` — calling the middleware factory *once*, producing the actual `func(http.Handler) http.Handler` (same "call it once to get the real middleware" pattern as `RequestLogger(logger)` in Lesson 2). - `r.Group(func(r chi.Router) { ... })` — chi's way of scoping middleware to a *subset* of routes instead of the whole router. Inside the group, `r.Use(requireAuth)` only applies to routes registered *within that same closure* — `/me` is protected, but `/register`/`/login`/`/logout` (registered outside the group) are not. Add future authenticated-only routes inside this same `r.Group(...)` block. ## Try it ```bash go run ./cmd/api ``` ```bash curl http://localhost:8080/me # {"error":"unauthorized"} curl -c cookies.txt -X POST http://localhost:8080/login \ -H "Content-Type: application/json" \ -d '{"email":"hamid@example.com","password":"secret123"}' curl -b cookies.txt http://localhost:8080/me # now works ``` Try logging out and hitting `/me` again — should go back to `unauthorized`, this time via the middleware instead of manual logic inside the handler. **A sanity check on your understanding:** if you comment out `r.Use(requireAuth)` inside the `Group`, `/me` will still correctly return `401` (via `Me`'s defensive `nil` check on `CurrentUser(r)`), not a crash — because `middleware.CurrentUser(r)` finds nothing in the context (the middleware never ran to put it there), and `Me`'s check catches that. Try it and read the log line that gets printed. Once the protected/unprotected split works, move to Lesson 9 — rate limiting & security hardening.