This skill should be used when the user asks to "integrate goth with echo", "oauth echo framework", "echo authentication", "goth session management", "oauth security", "secure oauth", "gorilla sessions", or needs help with session storage, security patterns, or Echo framework integration for Goth.
Provides expert guidance for integrating Goth authentication with Echo framework and implementing secure session management. Use when users need Echo route handlers, middleware, or secure session storage patterns for OAuth integration.
/plugin marketplace add linehaul-ai/linehaulai-claude-marketplace/plugin install goth-oauth@linehaulai-claude-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Expert guidance for integrating github.com/markbates/goth with the Echo web framework and implementing secure session management.
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")
}
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
}
}
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, "/")
}
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)
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
}
Generate a secure session secret:
# Generate 32-byte random secret
openssl rand -base64 32
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")
}
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
}
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
}
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.
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")
}
// ...
}
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
}
In production, always use HTTPS:
Secure: true on cookies// Echo HTTPS redirect middleware
e.Pre(middleware.HTTPSRedirect())
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
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")
}
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
)))
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
}
Before deploying:
Secure: true in productionHttpOnly: trueSameSite: Lax or StrictSee references/security-checklist.md for complete checklist.
| 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 |
references/session-storage-options.md - Storage comparisonreferences/security-checklist.md - Security verification