Introduction
Setting cookies in a cross-origin setup (Go backend on port 4000, JavaScript frontend on port 3000) requires careful configuration on both sides. The Fetch API doesn’t send or accept cookies by default โ you must explicitly enable credentials: 'include'. The Go server must also set the correct CORS headers.
Go Backend: Setting Cookies
Basic Cookie Setup
package main
import (
"net/http"
"time"
)
func loginHandler(w http.ResponseWriter, r *http.Request) {
// Authenticate user...
// Set the session cookie
cookie := &http.Cookie{
Name: "session_token",
Value: "abc123xyz",
Path: "/",
HttpOnly: true, // not accessible via JavaScript
Secure: true, // HTTPS only (required in production)
SameSite: http.SameSiteLaxMode, // CSRF protection
MaxAge: int((24 * time.Hour).Seconds()), // 24 hours
}
http.SetCookie(w, cookie)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status": "logged in"}`))
}
Cookie Attributes Explained
| Attribute | Purpose | Recommendation |
|---|---|---|
HttpOnly |
Prevents JavaScript access (XSS protection) | Always set to true |
Secure |
Only sent over HTTPS | true in production, false in local dev |
SameSite |
Controls cross-site sending | Lax for most apps, Strict for sensitive |
MaxAge |
Expiry in seconds | Set appropriate lifetime |
Path |
Which paths the cookie applies to | Usually / |
Domain |
Which domains receive the cookie | Leave empty for current domain |
SameSite Values
http.SameSiteStrictMode // Never sent cross-site (most secure, breaks some flows)
http.SameSiteLaxMode // Sent on top-level navigation (recommended default)
http.SameSiteNoneMode // Always sent cross-site (requires Secure=true)
For cross-origin requests (frontend on different domain/port), you need SameSiteNoneMode with Secure: true:
cookie := &http.Cookie{
Name: "session_token",
Value: token,
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode, // required for cross-origin
MaxAge: 86400,
}
CORS Configuration in Go
For cross-origin requests with cookies, the server must set specific CORS headers:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Allow specific origins (never use "*" with credentials)
allowedOrigins := map[string]bool{
"http://localhost:3000": true,
"https://myapp.com": true,
}
if allowedOrigins[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
// CRITICAL: must be "true" (not "*") when using credentials
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
// Handle preflight requests
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/login", loginHandler)
mux.HandleFunc("/profile", profileHandler)
handler := corsMiddleware(mux)
http.ListenAndServe(":4000", handler)
}
Critical rule: When credentials: 'include' is used, Access-Control-Allow-Origin cannot be * โ it must be the specific origin.
JavaScript Frontend: Fetch with Credentials
// Login request โ sends credentials and accepts cookies
async function login(email, password) {
const response = await fetch('http://localhost:4000/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({ email, password }),
credentials: 'include', // CRITICAL: send and accept cookies
mode: 'cors',
cache: 'no-cache',
});
if (!response.ok) {
throw new Error(`Login failed: ${response.status}`);
}
return response.json();
}
// Subsequent authenticated request โ cookie sent automatically
async function getProfile() {
const response = await fetch('http://localhost:4000/profile', {
credentials: 'include', // sends the session cookie
});
return response.json();
}
// Logout โ clear the cookie
async function logout() {
await fetch('http://localhost:4000/logout', {
method: 'POST',
credentials: 'include',
});
}
credentials Options
| Value | Behavior |
|---|---|
'omit' |
Never send cookies (default) |
'same-origin' |
Send cookies only to same origin |
'include' |
Always send cookies (required for cross-origin) |
Reading Cookies in Go
func profileHandler(w http.ResponseWriter, r *http.Request) {
// Read a specific cookie
cookie, err := r.Cookie("session_token")
if err != nil {
if err == http.ErrNoCookie {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
token := cookie.Value
// Validate token and get user...
w.Write([]byte(`{"user": "alice"}`))
}
Deleting Cookies
To delete a cookie, set it with MaxAge: -1 or Expires in the past:
func logoutHandler(w http.ResponseWriter, r *http.Request) {
// Delete the session cookie
http.SetCookie(w, &http.Cookie{
Name: "session_token",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
MaxAge: -1, // delete immediately
})
w.Write([]byte(`{"status": "logged out"}`))
}
Complete Example
package main
import (
"encoding/json"
"net/http"
"time"
)
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
// Validate credentials (simplified)
if req.Email != "[email protected]" || req.Password != "password" {
http.Error(w, "Invalid credentials", http.StatusUnauthorized)
return
}
// Set session cookie
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: "user-session-token-here",
Path: "/",
HttpOnly: true,
Secure: false, // true in production
SameSite: http.SameSiteLaxMode,
MaxAge: int((24 * time.Hour).Seconds()),
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
mux.HandleFunc("/me", func(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session")
if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate session token...
_ = cookie.Value
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"email": "[email protected]"})
})
http.ListenAndServe(":4000", corsMiddleware(mux))
}
Troubleshooting
| Problem | Cause | Fix |
|---|---|---|
| Cookie not set | Secure=true on HTTP |
Use HTTPS or set Secure=false in dev |
| Cookie not sent | Missing credentials: 'include' |
Add to all Fetch calls |
| CORS error | Allow-Origin: * with credentials |
Use specific origin, not * |
| Cookie blocked | SameSite=Strict cross-origin |
Use SameSite=None; Secure |
| Cookie not visible in DevTools | HttpOnly=true |
Expected โ HttpOnly hides from JS |
Comments