Echo Authentication Storage
Authentication is a critical component of web applications, and managing how user credentials and session data are stored is essential for both security and user experience. In this guide, we'll explore various methods for storing authentication data in Echo framework applications.
Introduction to Authentication Storage
Authentication storage refers to how an application maintains user authentication state across requests. In Echo applications, you have several options for storing this information:
- Cookies: Small pieces of data stored in the user's browser
- Sessions: Server-side storage mechanisms with client-side references
- JWT (JSON Web Tokens): Self-contained tokens that carry user information
- Database Storage: Persisting authentication data in databases
Each storage mechanism has its own advantages and trade-offs. Let's explore them in detail.
Cookie-Based Authentication Storage
Cookies are one of the simplest ways to store authentication information. They're stored in the client's browser and sent with each request.
Setting Up Cookie Storage
First, let's see how to implement basic cookie authentication in Echo:
package main
import (
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/login", login)
e.GET("/protected", protected, requireAuth)
e.Logger.Fatal(e.Start(":1323"))
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// In a real app, validate credentials against a database
if username == "admin" && password == "password" {
// Create a cookie that expires in 24 hours
cookie := new(http.Cookie)
cookie.Name = "user"
cookie.Value = username
cookie.Expires = time.Now().Add(24 * time.Hour)
cookie.Path = "/"
cookie.HttpOnly = true // Helps prevent XSS attacks
// Set the cookie in the response
c.SetCookie(cookie)
return c.JSON(http.StatusOK, map[string]string{
"message": "Login successful",
})
}
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid credentials",
})
}
func requireAuth(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie("user")
if err != nil || cookie.Value == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Please login to access this resource",
})
}
return next(c)
}
}
func protected(c echo.Context) error {
cookie, _ := c.Cookie("user")
username := cookie.Value
return c.JSON(http.StatusOK, map[string]string{
"message": "Welcome to the protected resource, " + username,
})
}
Security Considerations for Cookies
When using cookies for authentication, consider these security practices:
- Set
HttpOnly
flag to prevent access via JavaScript - Set
Secure
flag to ensure cookies are only sent over HTTPS - Set appropriate expiration times
- Consider using signed cookies to prevent tampering
Session-Based Authentication
Sessions extend cookie-based authentication by storing most data on the server side, with only a session identifier stored in a cookie.
Implementing Sessions with Echo
Let's implement session-based authentication using the gorilla/sessions
package:
package main
import (
"net/http"
"github.com/gorilla/sessions"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Store instance for sessions
var store = sessions.NewCookieStore([]byte("secret-key-that-should-be-in-env-variable"))
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/login", login)
e.GET("/logout", logout)
e.GET("/protected", protected, requireAuth)
e.Logger.Fatal(e.Start(":1323"))
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// In a real app, validate credentials against a database
if username == "admin" && password == "password" {
// Get a session. We're ignoring the error resulted from decoding an
// existing session: Get() always returns a session, even if empty.
session, _ := store.Get(c.Request(), "session-name")
// Set session values
session.Values["authenticated"] = true
session.Values["username"] = username
// Save it
session.Save(c.Request(), c.Response())
return c.JSON(http.StatusOK, map[string]string{
"message": "Login successful",
})
}
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid credentials",
})
}
func logout(c echo.Context) error {
session, _ := store.Get(c.Request(), "session-name")
// Revoke users authentication
session.Values["authenticated"] = false
session.Save(c.Request(), c.Response())
return c.JSON(http.StatusOK, map[string]string{
"message": "Logged out successfully",
})
}
func requireAuth(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
session, _ := store.Get(c.Request(), "session-name")
// Check if authenticated
auth, ok := session.Values["authenticated"].(bool)
if !ok || !auth {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Please login to access this resource",
})
}
return next(c)
}
}
func protected(c echo.Context) error {
session, _ := store.Get(c.Request(), "session-name")
username := session.Values["username"].(string)
return c.JSON(http.StatusOK, map[string]string{
"message": "Welcome to the protected resource, " + username,
})
}
Session Storage Options
Sessions can be stored in various backends:
- Memory: Fast but lost on server restart
- Redis: Great for distributed systems
- Database: Persistent but slower access
JWT-Based Authentication
JSON Web Tokens (JWTs) provide a stateless approach to authentication, where all necessary information is contained within the token itself.
Creating and Validating JWTs in Echo
package main
import (
"net/http"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// JWT secret key
var jwtSecret = []byte("your-secret-key-should-be-in-env-vars")
// User claims for JWT
type JWTCustomClaims struct {
Username string `json:"username"`
Admin bool `json:"admin"`
jwt.RegisteredClaims
}
func main() {
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/login", login)
// Restricted group
r := e.Group("/restricted")
// Configure JWT middleware
config := middleware.JWTConfig{
Claims: &JWTCustomClaims{},
SigningKey: jwtSecret,
}
r.Use(middleware.JWTWithConfig(config))
r.GET("", restricted)
e.Logger.Fatal(e.Start(":1323"))
}
func login(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// In a real app, validate credentials against a database
if username == "admin" && password == "password" {
// Set custom claims
claims := &JWTCustomClaims{
username,
true,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
Issuer: "echo-auth-server",
Subject: username,
},
}
// Create token with claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// Generate encoded token
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error generating token",
})
}
return c.JSON(http.StatusOK, map[string]string{
"token": tokenString,
})
}
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid credentials",
})
}
func restricted(c echo.Context) error {
user := c.Get("user").(*jwt.Token)
claims := user.Claims.(*JWTCustomClaims)
username := claims.Username
return c.JSON(http.StatusOK, map[string]string{
"message": "Welcome to the restricted area, " + username,
})
}
Using JWTs in Client Requests
Once your server issues a JWT, clients should include it in their requests:
// JavaScript example (using fetch API)
fetch('https://api.example.com/restricted', {
method: 'GET',
headers: {
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
}
})
.then(response => response.json())
.then(data => console.log(data));
Database Authentication Storage
For more complex applications, storing authentication data in a database provides flexibility and persistence.
Integrating with a Database
Here's an example using GORM with a PostgreSQL database:
package main
import (
"fmt"
"net/http"
"time"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// User model for the database
type User struct {
gorm.Model
Username string `gorm:"uniqueIndex;not null"`
PasswordHash string `gorm:"not null"`
Email string `gorm:"uniqueIndex"`
LastLogin time.Time
}
// Session model for persistent sessions
type Session struct {
gorm.Model
UserID uint `gorm:"not null"`
Token string `gorm:"uniqueIndex;not null"`
ExpiresAt time.Time
}
var db *gorm.DB
func initDB() {
var err error
dsn := "host=localhost user=postgres password=password dbname=auth port=5432 sslmode=disable"
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
panic("failed to connect to database")
}
// Auto migrate schemas
db.AutoMigrate(&User{}, &Session{})
}
func main() {
// Initialize database
initDB()
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.POST("/register", registerUser)
e.POST("/login", loginUser)
e.GET("/protected", protectedResource, authMiddleware)
e.Logger.Fatal(e.Start(":1323"))
}
func registerUser(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
email := c.FormValue("email")
// Hash the password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"message": "Error processing request",
})
}
// Create user
user := User{
Username: username,
PasswordHash: string(hashedPassword),
Email: email,
}
// Save to DB
result := db.Create(&user)
if result.Error != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"message": "Error creating user, possibly duplicate username",
})
}
return c.JSON(http.StatusCreated, map[string]string{
"message": "User registered successfully",
})
}
func loginUser(c echo.Context) error {
username := c.FormValue("username")
password := c.FormValue("password")
// Find user
var user User
if err := db.Where("username = ?", username).First(&user).Error; err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid credentials",
})
}
// Check password
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)); err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid credentials",
})
}
// Generate session token
token := fmt.Sprintf("%d%d", time.Now().UnixNano(), user.ID)
hashedToken, _ := bcrypt.GenerateFromPassword([]byte(token), bcrypt.DefaultCost)
tokenString := fmt.Sprintf("%x", hashedToken)[:32]
// Create session
session := Session{
UserID: user.ID,
Token: tokenString,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
db.Create(&session)
// Update last login time
user.LastLogin = time.Now()
db.Save(&user)
// Set cookie
cookie := new(http.Cookie)
cookie.Name = "session_token"
cookie.Value = tokenString
cookie.Expires = session.ExpiresAt
cookie.HttpOnly = true
cookie.Path = "/"
c.SetCookie(cookie)
return c.JSON(http.StatusOK, map[string]string{
"message": "Login successful",
})
}
func authMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
cookie, err := c.Cookie("session_token")
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Authentication required",
})
}
// Find session
var session Session
if err := db.Where("token = ? AND expires_at > ?", cookie.Value, time.Now()).First(&session).Error; err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "Invalid or expired session",
})
}
// Find user
var user User
if err := db.First(&user, session.UserID).Error; err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"message": "User not found",
})
}
// Set user in context
c.Set("user", user)
return next(c)
}
}
func protectedResource(c echo.Context) error {
user := c.Get("user").(User)
return c.JSON(http.StatusOK, map[string]string{
"message": "Welcome to the protected resource, " + user.Username,
})
}
Choosing the Right Storage Mechanism
When selecting an authentication storage method, consider:
Storage Type | Pros | Cons | Best For |
---|---|---|---|
Cookies | Simple, built-in | Limited storage, vulnerable to XSS/CSRF | Small applications |
Sessions | More secure, flexible storage | Server-side overhead | Medium applications |
JWTs | Stateless, scalable | Size, can't be invalidated easily | API services |
Database | Persistent, queryable | Performance overhead | Complex applications |
Best Practices for Authentication Storage
- Never store plaintext passwords - Always hash passwords using bcrypt or similar algorithms
- Use HTTPS for all authentication requests
- Set appropriate expiration times for sessions and tokens
- Implement CSRF protection when using cookie-based authentication
- Provide secure logout functionality
- Consider implementing two-factor authentication for added security
- Rate limit authentication attempts to prevent brute force attacks
Summary
Authentication storage is a critical aspect of web application security. Echo provides flexible options for implementing various storage mechanisms:
- Cookie-based storage is simple but requires careful security consideration
- Session-based storage provides better security with server-side control
- JWT-based authentication offers stateless operation ideal for APIs
- Database storage gives you maximum flexibility and control
Choose the method that best suits your application's requirements, considering factors like security needs, scalability, and user experience.
Additional Resources
- Echo Framework Documentation
- OWASP Authentication Cheat Sheet
- JWT.io - For learning about JWT structure and validation
Exercises
- Implement a combined JWT and cookie authentication system that provides both stateless API access and traditional web session management.
- Create a password reset flow using time-limited tokens stored in a database.
- Extend the database example to include role-based access control (RBAC).
- Implement refresh tokens alongside access tokens in the JWT example.
- Add two-factor authentication to any of the examples using a library like
rsc.io/2fa
.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)