Goth Echo Integration & Security
Expert guidance for integrating github.com/markbates/goth with the Echo web framework and implementing secure session management.
Echo Framework Integration
Basic Route Setup
import ( "github.com/labstack/echo/v4" "github.com/markbates/goth" "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/google" )
func main() { e := echo.New()
// Auth routes
e.GET("/auth/:provider", handleAuth)
e.GET("/auth/:provider/callback", handleCallback)
e.GET("/logout", handleLogout)
e.Start(":3000")
}
Provider Name from Echo Context
Override Gothic's provider getter to use Echo's path parameters:
func init() { gothic.GetProviderName = func(r *http.Request) (string, error) { // Extract from Echo's :provider path param // The request context contains Echo's params provider := r.URL.Query().Get(":provider") if provider == "" { // Fallback: parse from path parts := strings.Split(r.URL.Path, "/") for i, p := range parts { if p == "auth" && i+1 < len(parts) { return parts[i+1], nil } } } if provider == "" { return "", errors.New("no provider specified") } return provider, nil } }
Echo Handler Wrappers
Wrap Gothic handlers for Echo compatibility:
func handleAuth(c echo.Context) error { // Set provider in query for Gothic q := c.Request().URL.Query() q.Set(":provider", c.Param("provider")) c.Request().URL.RawQuery = q.Encode()
gothic.BeginAuthHandler(c.Response(), c.Request())
return nil
}
func handleCallback(c echo.Context) error { q := c.Request().URL.Query() q.Set(":provider", c.Param("provider")) c.Request().URL.RawQuery = q.Encode()
user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
if err != nil {
return c.String(http.StatusInternalServerError, err.Error())
}
// Store user in session, redirect to dashboard
return c.JSON(http.StatusOK, map[string]interface{}{
"name": user.Name,
"email": user.Email,
})
}
func handleLogout(c echo.Context) error { gothic.Logout(c.Response(), c.Request()) return c.Redirect(http.StatusTemporaryRedirect, "/") }
Echo Middleware for Auth
Create middleware to protect routes:
func RequireAuth(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { session, err := gothic.Store.Get(c.Request(), gothic.SessionName) if err != nil || session.Values["user_id"] == nil { return c.Redirect(http.StatusTemporaryRedirect, "/login") } return next(c) } }
// Usage e.GET("/dashboard", handleDashboard, RequireAuth)
Session Management
Default Cookie Store
Gothic uses gorilla/sessions CookieStore by default:
import "github.com/gorilla/sessions"
func initSessionStore() { key := []byte(os.Getenv("SESSION_SECRET")) if len(key) < 32 { log.Fatal("SESSION_SECRET must be at least 32 bytes") }
store := sessions.NewCookieStore(key)
store.MaxAge(86400 * 30) // 30 days
store.Options.Path = "/"
store.Options.HttpOnly = true
store.Options.Secure = os.Getenv("ENV") == "production"
store.Options.SameSite = http.SameSiteLaxMode
gothic.Store = store
}
Session Secret Generation
Generate a secure session secret:
Generate 32-byte random secret
openssl rand -base64 32
Storing User Data in Session
After successful authentication:
func handleCallback(c echo.Context) error { user, err := gothic.CompleteUserAuth(c.Response(), c.Request()) if err != nil { return err }
// Get or create session
session, _ := gothic.Store.Get(c.Request(), "user-session")
// Store user data
session.Values["user_id"] = user.UserID
session.Values["email"] = user.Email
session.Values["name"] = user.Name
session.Values["access_token"] = user.AccessToken
session.Values["provider"] = user.Provider
// Save session
if err := session.Save(c.Request(), c.Response()); err != nil {
return err
}
return c.Redirect(http.StatusTemporaryRedirect, "/dashboard")
}
Retrieving User from Session
func getCurrentUser(c echo.Context) (*UserInfo, error) { session, err := gothic.Store.Get(c.Request(), "user-session") if err != nil { return nil, err }
userID, ok := session.Values["user_id"].(string)
if !ok || userID == "" {
return nil, errors.New("not authenticated")
}
return &UserInfo{
UserID: userID,
Email: session.Values["email"].(string),
Name: session.Values["name"].(string),
Provider: session.Values["provider"].(string),
}, nil
}
Alternative Session Stores
Redis Session Store
For distributed deployments:
import "github.com/rbcervilla/redisstore/v9"
func initRedisStore() { client := redis.NewClient(&redis.Options{ Addr: os.Getenv("REDIS_URL"), })
store, err := redisstore.NewRedisStore(context.Background(), client)
if err != nil {
log.Fatal(err)
}
store.KeyPrefix("session_")
store.Options(sessions.Options{
Path: "/",
MaxAge: 86400 * 30,
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteLaxMode,
})
gothic.Store = store
}
Database Session Store
For PostgreSQL with pgx:
import "github.com/antonlindstrom/pgstore"
func initPgStore() { store, err := pgstore.NewPGStoreFromPool( dbPool, []byte(os.Getenv("SESSION_SECRET")), ) if err != nil { log.Fatal(err) }
store.Options = &sessions.Options{
Path: "/",
MaxAge: 86400 * 30,
HttpOnly: true,
Secure: true,
}
gothic.Store = store
}
See references/session-storage-options.md for detailed comparison.
Security Best Practices
CSRF Protection with State Parameter
Goth automatically handles the OAuth state parameter for CSRF protection. Verify it's working:
// Gothic handles state internally, but verify in callback func handleCallback(c echo.Context) error { // State is validated by gothic.CompleteUserAuth user, err := gothic.CompleteUserAuth(c.Response(), c.Request()) if err != nil { // State mismatch will cause error here log.Printf("Auth failed (possible CSRF): %v", err) return c.Redirect(http.StatusTemporaryRedirect, "/login?error=invalid_state") } // ... }
Secure Cookie Configuration
store.Options = &sessions.Options{ Path: "/", Domain: "", // Current domain only MaxAge: 86400 * 7, // 7 days Secure: true, // HTTPS only HttpOnly: true, // No JavaScript access SameSite: http.SameSiteLaxMode, // CSRF protection }
HTTPS Requirements
In production, always use HTTPS:
-
Set Secure: true on cookies
-
Use HTTPS callback URLs in provider configuration
-
Redirect HTTP to HTTPS
// Echo HTTPS redirect middleware e.Pre(middleware.HTTPSRedirect())
Token Storage Security
Never expose access tokens to the client:
// DON'T: Send token to frontend return c.JSON(200, map[string]string{ "access_token": user.AccessToken, // Dangerous! })
// DO: Store token server-side only session.Values["access_token"] = user.AccessToken
Session Hijacking Prevention
Regenerate session ID after authentication:
func handleCallback(c echo.Context) error { user, err := gothic.CompleteUserAuth(c.Response(), c.Request()) if err != nil { return err }
// Get existing session
oldSession, _ := gothic.Store.Get(c.Request(), "user-session")
// Copy values to new session (forces new ID)
oldSession.Options.MaxAge = -1 // Delete old session
oldSession.Save(c.Request(), c.Response())
newSession, _ := gothic.Store.New(c.Request(), "user-session")
newSession.Values["user_id"] = user.UserID
newSession.Values["email"] = user.Email
newSession.Save(c.Request(), c.Response())
return c.Redirect(http.StatusTemporaryRedirect, "/dashboard")
}
Rate Limiting Auth Endpoints
Protect against brute force:
import "github.com/labstack/echo/v4/middleware"
// Limit auth endpoints authGroup := e.Group("/auth") authGroup.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore( rate.Limit(10), // 10 requests per second )))
Token Refresh Pattern
Keep access tokens fresh:
func refreshTokenIfNeeded(c echo.Context) error { session, _ := gothic.Store.Get(c.Request(), "user-session")
expiresAt, ok := session.Values["expires_at"].(time.Time)
if !ok || time.Until(expiresAt) > 5*time.Minute {
return nil // Token still valid
}
providerName := session.Values["provider"].(string)
provider, _ := goth.GetProvider(providerName)
if !provider.RefreshTokenAvailable() {
return nil
}
refreshToken := session.Values["refresh_token"].(string)
token, err := provider.RefreshToken(refreshToken)
if err != nil {
// Refresh failed - force re-login
return c.Redirect(http.StatusTemporaryRedirect, "/logout")
}
session.Values["access_token"] = token.AccessToken
session.Values["expires_at"] = token.Expiry
if token.RefreshToken != "" {
session.Values["refresh_token"] = token.RefreshToken
}
session.Save(c.Request(), c.Response())
return nil
}
Security Checklist
Before deploying:
-
SESSION_SECRET is at least 32 random bytes
-
Cookies use Secure: true in production
-
Cookies use HttpOnly: true
-
Cookies use SameSite: Lax or Strict
-
HTTPS is enforced in production
-
Callback URLs use HTTPS
-
Access tokens stored server-side only
-
Rate limiting on auth endpoints
-
Session regeneration after login
-
Error messages don't leak sensitive info
See references/security-checklist.md for complete checklist.
Quick Reference
Task Code
Set session store gothic.Store = store
Get session gothic.Store.Get(r, "name")
Save session session.Save(r, w)
Delete session session.Options.MaxAge = -1
Secure cookie Secure: true, HttpOnly: true
Related Skills
-
goth-fundamentals - Core Goth concepts
-
goth-providers - Provider configuration
Reference Documentation
-
references/session-storage-options.md
-
Storage comparison
-
references/security-checklist.md
-
Security verification