Echo Password Hashing
Introduction
Password security is one of the most critical aspects of web application development. Storing passwords in plain text exposes your users to significant risks if your database is ever compromised. This is where password hashing comes in - a one-way transformation of a password into a string of characters that cannot be reversed to reveal the original password.
In this guide, you'll learn how to implement secure password hashing in your Echo applications using Go's built-in cryptographic libraries and popular third-party packages. We'll cover the fundamentals of password hashing, best practices, and provide practical examples you can apply to your own projects.
Password Hashing Fundamentals
What is Password Hashing?
Password hashing is a security technique where a password is converted into a fixed-length string of characters through a mathematical algorithm. Unlike encryption, hashing is designed to be a one-way process - it should be practically impossible to convert the hash back to the original password.
Why Hash Passwords?
- Data Protection - Even if your database is compromised, attackers only get hashed versions of passwords
- Compliance - Many regulations and standards (GDPR, HIPAA, PCI DSS) require secure password storage
- User Trust - Demonstrates commitment to security best practices
Choosing a Password Hashing Algorithm
For modern web applications, the recommended hashing algorithms include:
- bcrypt - Designed specifically for password hashing, includes built-in salting
- Argon2 - Winner of the Password Hashing Competition in 2015, stronger but more resource-intensive
- scrypt - Designed to be more resistant to hardware brute-force attacks
We'll focus on bcrypt in this guide as it provides a good balance of security and performance.
Implementing Password Hashing in Echo
Setting Up Your Project
First, let's set up a basic Echo project with the bcrypt library:
mkdir echo-password-hashing
cd echo-password-hashing
go mod init echo-password-hashing
go get github.com/labstack/echo/v4
go get golang.org/x/crypto/bcrypt
Creating a User Model
Let's define a simple user model to store user credentials:
package models
import (
"golang.org/x/crypto/bcrypt"
)
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"` // The "-" ensures the password won't be included in JSON responses
}
// HashPassword hashes the user's password using bcrypt
func (u *User) HashPassword(password string) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
return nil
}
// CheckPassword compares the provided password with the stored hash
func (u *User) CheckPassword(password string) error {
return bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
}
Creating Registration and Login Handlers
Now, let's implement the registration and login handlers in Echo:
package handlers
import (
"net/http"
"echo-password-hashing/models"
"github.com/labstack/echo/v4"
)
// For simplicity, we'll store users in memory
// In a real application, you would use a database
var users = make(map[string]models.User)
var nextID uint = 1
type RegisterRequest struct {
Username string `json:"username" validate:"required"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
type LoginRequest struct {
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
func Register(c echo.Context) error {
req := new(RegisterRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
}
// Check if username already exists
if _, exists := users[req.Username]; exists {
return c.JSON(http.StatusConflict, map[string]string{"error": "Username already taken"})
}
// Create new user
user := models.User{
ID: nextID,
Username: req.Username,
Email: req.Email,
}
nextID++
// Hash the password
if err := user.HashPassword(req.Password); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to hash password"})
}
// Store user
users[req.Username] = user
return c.JSON(http.StatusCreated, map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"message": "User registered successfully",
})
}
func Login(c echo.Context) error {
req := new(LoginRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
}
// Find user by username
user, exists := users[req.Username]
if !exists {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
}
// Verify password
if err := user.CheckPassword(req.Password); err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
}
return c.JSON(http.StatusOK, map[string]interface{}{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"message": "Login successful",
})
}
Setting Up Routes
Now, let's set up the Echo server with our routes:
package main
import (
"echo-password-hashing/handlers"
"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("/register", handlers.Register)
e.POST("/login", handlers.Login)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
Testing the Implementation
Let's test our implementation using cURL commands:
Register a user
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "email": "[email protected]", "password": "securepassword123"}'
Expected output:
{
"id": 1,
"username": "johndoe",
"email": "[email protected]",
"message": "User registered successfully"
}
Login with correct credentials
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "password": "securepassword123"}'
Expected output:
{
"id": 1,
"username": "johndoe",
"email": "[email protected]",
"message": "Login successful"
}
Login with incorrect password
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "password": "wrongpassword"}'
Expected output:
{
"error": "Invalid credentials"
}
Password Hashing Best Practices
- Never store plain-text passwords - This should be non-negotiable
- Use appropriate work factors - Adjust bcrypt cost parameter based on your server's capabilities
- Implement rate limiting - Prevent brute force attacks by limiting login attempts
- Add server-side validation - Enforce strong password policies
- Update hashing algorithms - As computing power increases, older algorithms become less secure
- Consider using Argon2id for new projects - It's the current state-of-the-art
Adjusting the Work Factor
The work factor (cost) determines how computationally intensive the hashing process will be. Higher values are more secure but take longer to compute:
// Instead of using bcrypt.DefaultCost (which is 10)
// You can choose a custom cost between 4 and 31
// Higher is more secure but slower
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 12)
Using Argon2id (Advanced)
For enhanced security, you might want to use Argon2id instead of bcrypt. Here's how to implement it using a third-party package:
go get github.com/alexedwards/argon2id
package models
import (
"github.com/alexedwards/argon2id"
)
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"-"`
}
// HashPassword hashes the user's password using Argon2id
func (u *User) HashPassword(password string) error {
// These parameters can be adjusted based on your security requirements
params := &argon2id.Params{
Memory: 64 * 1024, // 64MB
Iterations: 3,
Parallelism: 2,
SaltLength: 16,
KeyLength: 32,
}
hash, err := argon2id.CreateHash(password, params)
if err != nil {
return err
}
u.Password = hash
return nil
}
// CheckPassword compares the provided password with the stored hash
func (u *User) CheckPassword(password string) (bool, error) {
return argon2id.ComparePasswordAndHash(password, u.Password)
}
Real-World Application: Password Reset Flow
Here's an example of how password hashing would be used in a password reset flow:
package handlers
import (
"net/http"
"time"
"echo-password-hashing/models"
"github.com/labstack/echo/v4"
)
type ResetPasswordRequest struct {
Username string `json:"username" validate:"required"`
OldPassword string `json:"old_password" validate:"required"`
NewPassword string `json:"new_password" validate:"required,min=8"`
}
func ResetPassword(c echo.Context) error {
req := new(ResetPasswordRequest)
if err := c.Bind(req); err != nil {
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
}
// Find user by username
user, exists := users[req.Username]
if !exists {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
}
// Verify old password
if err := user.CheckPassword(req.OldPassword); err != nil {
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid credentials"})
}
// Hash and set new password
if err := user.HashPassword(req.NewPassword); err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to update password"})
}
// Update user in storage
users[req.Username] = user
return c.JSON(http.StatusOK, map[string]string{"message": "Password updated successfully"})
}
Summary
Password hashing is an essential security practice for any web application that handles user authentication. In this guide, we've covered:
- The fundamentals of password hashing and why it's crucial
- How to implement secure password hashing in Echo using bcrypt
- Creating user registration and login handlers with password verification
- Advanced techniques using Argon2id for enhanced security
- Best practices for password hashing in production environments
By following these practices, you'll significantly enhance the security of your Echo application and protect your users' credentials even in the event of a data breach.
Additional Resources
- Go bcrypt Documentation
- OWASP Password Storage Cheat Sheet
- Argon2 Password Hashing Specification
- NIST Digital Identity Guidelines
Exercises
-
Basic: Modify the registration handler to enforce a stronger password policy (minimum length, required character types).
-
Intermediate: Implement a rate-limiting middleware to prevent brute-force attacks on the login endpoint.
-
Advanced: Create a complete user authentication system with JWT token generation after successful login, and protected routes that require authentication.
-
Challenge: Implement a password migration system that can upgrade password hashes from an older algorithm (like SHA-256) to bcrypt or Argon2id when users log in.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)