Skip to main content

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.

Cookies are one of the simplest ways to store authentication information. They're stored in the client's browser and sent with each request.

First, let's see how to implement basic cookie authentication in Echo:

go
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:

go
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:

  1. Memory: Fast but lost on server restart
  2. Redis: Great for distributed systems
  3. 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

go
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
// 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:

go
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 TypeProsConsBest For
CookiesSimple, built-inLimited storage, vulnerable to XSS/CSRFSmall applications
SessionsMore secure, flexible storageServer-side overheadMedium applications
JWTsStateless, scalableSize, can't be invalidated easilyAPI services
DatabasePersistent, queryablePerformance overheadComplex applications

Best Practices for Authentication Storage

  1. Never store plaintext passwords - Always hash passwords using bcrypt or similar algorithms
  2. Use HTTPS for all authentication requests
  3. Set appropriate expiration times for sessions and tokens
  4. Implement CSRF protection when using cookie-based authentication
  5. Provide secure logout functionality
  6. Consider implementing two-factor authentication for added security
  7. 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

Exercises

  1. Implement a combined JWT and cookie authentication system that provides both stateless API access and traditional web session management.
  2. Create a password reset flow using time-limited tokens stored in a database.
  3. Extend the database example to include role-based access control (RBAC).
  4. Implement refresh tokens alongside access tokens in the JWT example.
  5. 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! :)