| name | goth-echo-security |
| description | 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. |
| keywords | ["goth","echo","oauth2","session-management","security"] |
| disable-model-invocation | false |
| user-invocable | true |
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()
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) {
provider := r.URL.Query().Get(":provider")
if provider == "" {
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 {
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())
}
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)
}
}
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)
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:
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
}
session, _ := gothic.Store.Get(c.Request(), "user-session")
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
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:
func handleCallback(c echo.Context) error {
user, err := gothic.CompleteUserAuth(c.Response(), c.Request())
if err != nil {
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: "",
MaxAge: 86400 * 7,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
HTTPS Requirements
In production, always use HTTPS:
- Set
Secure: true on cookies
- Use HTTPS callback URLs in provider configuration
- Redirect HTTP to HTTPS
e.Pre(middleware.HTTPSRedirect())
Token Storage Security
Never expose access tokens to the client:
return c.JSON(200, map[string]string{
"access_token": user.AccessToken,
})
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
}
oldSession, _ := gothic.Store.Get(c.Request(), "user-session")
oldSession.Options.MaxAge = -1
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"
authGroup := e.Group("/auth")
authGroup.Use(middleware.RateLimiter(middleware.NewRateLimiterMemoryStore(
rate.Limit(10),
)))
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
}
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 {
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:
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