Auth

Middleware-based authentication with context-injected users.

The GOaT stack uses middleware to handle authentication. The pattern works with any auth provider (WorkOS, Auth0, Clerk, or roll your own).

The Middleware Pattern

Two middleware functions cover all cases:

type AuthMiddleware struct {
    Store   *sessions.CookieStore
    JWKS    *keyfunc.JWKS
    UserSvc *service.UserService
}

// Auth — required. Redirects to /login if not authenticated.
func (m *AuthMiddleware) Auth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := m.getUserFromSession(r)
        if err != nil {
            http.Redirect(w, r, "/login", http.StatusFound)
            return
        }
        ctx := context.WithValue(r.Context(), UserContextKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// OptionalAuth — populates user if logged in, continues either way.
func (m *AuthMiddleware) OptionalAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        user, err := m.getUserFromSession(r)
        if err == nil {
            ctx := context.WithValue(r.Context(), UserContextKey, user)
            r = r.WithContext(ctx)
        }
        next.ServeHTTP(w, r)
    })
}

Context-Based User Access

Handlers access the user through context:

type contextKey string
const UserContextKey contextKey = "user_context"

// In any handler:
func (h *Handler) Dashboard(w http.ResponseWriter, r *http.Request) {
    user := r.Context().Value(middleware.UserContextKey).(*model.User)
    // user is guaranteed non-nil because this route uses Auth middleware
}

// In optional auth routes:
func (h *Handler) GamePage(w http.ResponseWriter, r *http.Request) {
    user, _ := r.Context().Value(middleware.UserContextKey).(*model.User)
    // user may be nil — check before using
}

Route Registration

Apply middleware at the route level:

// Public routes — no auth
mux.HandleFunc("GET /login", handlers.Auth.Login)

// Optional auth — user populated if available
mux.Handle("GET /game/{id}", auth.OptionalAuth(http.HandlerFunc(handlers.Game.Show)))

// Required auth — redirects to login
protected := http.NewServeMux()
protected.HandleFunc("POST /game/new", handlers.Game.Create)
mux.Handle("/", auth.Auth(protected))

Session Management

Store auth tokens in encrypted cookies:

store := sessions.NewCookieStore([]byte(sessionSecret))
store.Options = &sessions.Options{
    Path:     "/",
    MaxAge:   86400 * 30, // 30 days
    HttpOnly: true,
    Secure:   true,
    SameSite: http.SameSiteLaxMode,
}

Token Refresh

Automatically refresh expired JWTs:

func (m *AuthMiddleware) getUserFromSession(r *http.Request) (*model.User, error) {
    session, _ := m.Store.Get(r, "app-session")
    accessToken, _ := session.Values["access_token"].(string)

    token, err := jwt.Parse(accessToken, m.JWKS.Keyfunc)
    if err != nil || !token.Valid {
        // Try refresh
        refreshToken, _ := session.Values["refresh_token"].(string)
        newTokens, err := refreshWithProvider(refreshToken)
        if err != nil {
            return nil, err
        }
        session.Values["access_token"] = newTokens.AccessToken
        session.Values["refresh_token"] = newTokens.RefreshToken
        session.Save(r, w)
    }

    uid := session.Values["user_id"].(int32)
    return m.UserSvc.GetUserByID(r.Context(), uid)
}

Template Access

Pass the user to templates for conditional rendering:

{{ if .User }}
  <span>{{ .User.Name }}</span>
  <a href="/logout">Logout</a>
{{ else }}
  <a href="/login">Sign In</a>
{{ end }}

Gotchas

  • Always use HttpOnly and Secure cookies. Never expose tokens to JavaScript.
  • OptionalAuth should never error. If anything fails, just continue without a user.
  • Context keys should be custom types, not bare strings, to avoid collisions.
  • Don’t store sensitive data in the session. Store the user ID, fetch the rest from the database.