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
HttpOnlyandSecurecookies. 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.