Echo User Management
User management is a critical component of most web applications, allowing you to register users, authenticate them, and control their access to different parts of your application. In this guide, we'll explore how to implement a comprehensive user management system in Echo, a high-performance web framework for Go.
Introduction
User management typically involves several key components:
- Registration: Allowing new users to sign up
- Authentication: Verifying user identity through credentials
- Authorization: Controlling what authenticated users can access
- Profile Management: Allowing users to update their information
- Session Management: Maintaining user state between requests
We'll build a simple but comprehensive user management system using Echo and common Go packages to handle these aspects.
Prerequisites
Before we begin, make sure you have:
- Go installed (version 1.16+)
- Basic understanding of the Echo framework
- Experience with databases (we'll use SQLite for simplicity)
Let's start by setting up our project.
Project Setup
Create a new Go project and install the necessary dependencies:
mkdir echo-user-management
cd echo-user-management
go mod init echo-user-management
go get github.com/labstack/echo/v4
go get github.com/mattn/go-sqlite3
go get golang.org/x/crypto/bcrypt
go get github.com/golang-jwt/jwt/v4
Database Schema
Let's define our user model and set up a simple database:
package models
import (
"database/sql"
"time"
_ "github.com/mattn/go-sqlite3"
)
type User struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // Never send password in JSON responses
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func InitDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./users.db")
if err != nil {
return nil, err
}
// Create users table if it doesn't exist
createTableSQL := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createTableSQL)
if err != nil {
return nil, err
}
return db, nil
}
User Registration
Now, let's implement user registration functionality:
package handlers
import (
"database/sql"
"net/http"
"time"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
"echo-user-management/models"
)
type UserHandler struct {
DB *sql.DB
}
type RegisterRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
func (h *UserHandler) Register(c echo.Context) error {
// Parse request
req := new(RegisterRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
// Validate input
if req.Username == "" || req.Email == "" || req.Password == "" {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Username, email, and password are required",
})
}
// Hash password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to process request",
})
}
// Insert user into database
query := `INSERT INTO users (username, email, password, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)`
now := time.Now()
result, err := h.DB.Exec(query, req.Username, req.Email, string(hashedPassword), now, now)
if err != nil {
// Check for duplicate entry errors
return c.JSON(http.StatusConflict, map[string]string{
"error": "Username or email already exists",
})
}
id, _ := result.LastInsertId()
return c.JSON(http.StatusCreated, map[string]interface{}{
"id": id,
"username": req.Username,
"email": req.Email,
"message": "User registered successfully",
})
}
User Authentication
Next, we'll implement user authentication using JWT (JSON Web Tokens):
package handlers
import (
"net/http"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"golang.org/x/crypto/bcrypt"
)
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type JwtCustomClaims struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
const JWTSecret = "your-super-secret-key" // In production, use environment variables
func (h *UserHandler) Login(c echo.Context) error {
// Parse request
req := new(LoginRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
// Find user from database
var user struct {
ID int64
Username string
Password string
}
query := "SELECT id, username, password FROM users WHERE username = ?"
err := h.DB.QueryRow(query, req.Username).Scan(&user.ID, &user.Username, &user.Password)
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}
// Validate password
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password))
if err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid credentials",
})
}
// Create JWT token
claims := &JwtCustomClaims{
user.ID,
user.Username,
jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(JWTSecret))
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Could not generate token",
})
}
// Return token
return c.JSON(http.StatusOK, map[string]string{
"token": tokenString,
})
}
Middleware for Authentication
Now let's create middleware to protect routes that require authentication:
package middleware
import (
"net/http"
"github.com/golang-jwt/jwt/v4"
"github.com/labstack/echo/v4"
"echo-user-management/handlers"
)
func JWTAuth(jwtSecret string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Authorization header required",
})
}
// Usually the header is in format "Bearer <token>"
tokenString := authHeader[7:] // Skip "Bearer "
// Parse token
token, err := jwt.ParseWithClaims(tokenString, &handlers.JwtCustomClaims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(jwtSecret), nil
})
if err != nil || !token.Valid {
return c.JSON(http.StatusUnauthorized, map[string]string{
"error": "Invalid or expired token",
})
}
// Get claims and set in context for handlers to use
claims := token.Claims.(*handlers.JwtCustomClaims)
c.Set("user", claims)
return next(c)
}
}
}
User Profile Management
Now, let's add functionality to get and update user profiles:
func (h *UserHandler) GetProfile(c echo.Context) error {
// Get user from JWT context
userClaims := c.Get("user").(*JwtCustomClaims)
// Query database for user details
var user models.User
query := "SELECT id, username, email, created_at, updated_at FROM users WHERE id = ?"
err := h.DB.QueryRow(query, userClaims.UserID).Scan(
&user.ID, &user.Username, &user.Email, &user.CreatedAt, &user.UpdatedAt)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to retrieve user profile",
})
}
return c.JSON(http.StatusOK, user)
}
type UpdateProfileRequest struct {
Email string `json:"email"`
Password string `json:"password,omitempty"`
}
func (h *UserHandler) UpdateProfile(c echo.Context) error {
// Get user from JWT context
userClaims := c.Get("user").(*JwtCustomClaims)
// Parse request
req := new(UpdateProfileRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{
"error": "Invalid request format",
})
}
// Start with base query
query := "UPDATE users SET updated_at = ?"
args := []interface{}{time.Now()}
// Conditionally add fields to update
if req.Email != "" {
query += ", email = ?"
args = append(args, req.Email)
}
if req.Password != "" {
// Hash new password
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to process request",
})
}
query += ", password = ?"
args = append(args, string(hashedPassword))
}
// Add WHERE clause and execute
query += " WHERE id = ?"
args = append(args, userClaims.UserID)
_, err := h.DB.Exec(query, args...)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to update profile",
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Profile updated successfully",
})
}
Setting Up Routes
Let's put everything together by setting up the routes:
package main
import (
"log"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"echo-user-management/handlers"
"echo-user-management/middleware"
"echo-user-management/models"
)
func main() {
// Initialize database
db, err := models.InitDB()
if err != nil {
log.Fatalf("Failed to initialize database: %v", err)
}
defer db.Close()
// Initialize Echo
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Initialize handlers
userHandler := &handlers.UserHandler{DB: db}
// Public routes
e.POST("/register", userHandler.Register)
e.POST("/login", userHandler.Login)
// Protected routes
r := e.Group("/api")
r.Use(middleware.JWTAuth(handlers.JWTSecret))
r.GET("/profile", userHandler.GetProfile)
r.PUT("/profile", userHandler.UpdateProfile)
// Start server
e.Start(":8080")
}
Practical Example: A Complete User Authentication Flow
Let's see how this works in a complete flow:
1. Registering a new user
Request:
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"username": "johnsmith", "email": "[email protected]", "password": "secure123"}'
Response:
{
"id": 1,
"username": "johnsmith",
"email": "[email protected]",
"message": "User registered successfully"
}
2. Logging in
Request:
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "johnsmith", "password": "secure123"}'
Response:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5zbWl0aCIsImV4cCI6MTY1MTQ0NzQyM30.Mst8TDth_3BHrCtLBwpQ4rC2h-JgNvW6GMYpJ9yia5Y"
}
3. Getting the user profile
Request:
curl -X GET http://localhost:8080/api/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5zbWl0aCIsImV4cCI6MTY1MTQ0NzQyM30.Mst8TDth_3BHrCtLBwpQ4rC2h-JgNvW6GMYpJ9yia5Y"
Response:
{
"id": 1,
"username": "johnsmith",
"email": "[email protected]",
"created_at": "2023-05-01T15:32:03Z",
"updated_at": "2023-05-01T15:32:03Z"
}
4. Updating the user profile
Request:
curl -X PUT http://localhost:8080/api/profile \
-H "Content-Type: application/json" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImpvaG5zbWl0aCIsImV4cCI6MTY1MTQ0NzQyM30.Mst8TDth_3BHrCtLBwpQ4rC2h-JgNvW6GMYpJ9yia5Y" \
-d '{"email": "[email protected]"}'
Response:
{
"message": "Profile updated successfully"
}
Best Practices for User Management
- Security First: Always hash passwords before storing them, and never store plain text passwords.
- Input Validation: Validate all user inputs to prevent injection attacks.
- Rate Limiting: Implement rate limiting on login attempts to prevent brute force attacks.
- HTTPS: Always use HTTPS in production to encrypt data in transit.
- Token Management: Set appropriate expiration times for tokens and implement refresh token mechanisms for longer sessions.
- Error Handling: Provide meaningful error messages to users without exposing sensitive information.
- Logging: Log authentication events, but be careful not to log sensitive information.
Expanding the System
You can expand this basic user management system by adding features like:
- Password reset functionality
- Email verification
- Role-based access control
- Multi-factor authentication
- Account deletion and GDPR compliance
- OAuth integration for social logins
Summary
In this guide, we've built a comprehensive user management system for your Echo application. We've covered:
- Setting up a database for user storage
- Implementing user registration with password hashing
- Creating a JWT-based authentication system
- Adding middleware to protect routes
- Implementing profile management
- Testing the complete authentication flow
By following these steps, you now have a solid foundation for user management in your Echo applications. This system can be extended and customized to meet the specific needs of your application.
Additional Resources
- Echo Framework Documentation
- JWT.io - Learn more about JWTs
- OWASP Authentication Cheat Sheet
- Go Crypto Package
Exercises
- Implement a password reset functionality using email verification.
- Add role-based access control to restrict certain endpoints.
- Implement account lockout after multiple failed login attempts.
- Create an endpoint for users to delete their accounts.
- Add refresh token functionality to extend user sessions securely.
Happy coding with Echo's user management capabilities!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)