Skip to main content

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?

  1. Data Protection - Even if your database is compromised, attackers only get hashed versions of passwords
  2. Compliance - Many regulations and standards (GDPR, HIPAA, PCI DSS) require secure password storage
  3. 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:

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

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

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

go
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

bash
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "email": "[email protected]", "password": "securepassword123"}'

Expected output:

json
{
"id": 1,
"username": "johndoe",
"email": "[email protected]",
"message": "User registered successfully"
}

Login with correct credentials

bash
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "password": "securepassword123"}'

Expected output:

json
{
"id": 1,
"username": "johndoe",
"email": "[email protected]",
"message": "Login successful"
}

Login with incorrect password

bash
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/json" \
-d '{"username": "johndoe", "password": "wrongpassword"}'

Expected output:

json
{
"error": "Invalid credentials"
}

Password Hashing Best Practices

  1. Never store plain-text passwords - This should be non-negotiable
  2. Use appropriate work factors - Adjust bcrypt cost parameter based on your server's capabilities
  3. Implement rate limiting - Prevent brute force attacks by limiting login attempts
  4. Add server-side validation - Enforce strong password policies
  5. Update hashing algorithms - As computing power increases, older algorithms become less secure
  6. 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:

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

bash
go get github.com/alexedwards/argon2id
go
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:

go
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

Exercises

  1. Basic: Modify the registration handler to enforce a stronger password policy (minimum length, required character types).

  2. Intermediate: Implement a rate-limiting middleware to prevent brute-force attacks on the login endpoint.

  3. Advanced: Create a complete user authentication system with JWT token generation after successful login, and protected routes that require authentication.

  4. 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! :)