Skip to content

Password Hashers

Password hashers provide secure, salted password hashing for your Ravyn application. Never store passwords in plain text. always hash them with a strong algorithm.

What You'll Learn

  • Using built-in password hashers
  • Hashing and verifying passwords
  • Creating custom hashers
  • Configuring hashers in settings

Quick Start

from ravyn.contrib.auth.hashers import make_password, check_password

# Hash a password
hashed = make_password("my-secret-password")
# Returns: "$bcrypt$2b$12$..."

# Verify a password
is_valid = check_password("my-secret-password", hashed)
# Returns: True

is_valid = check_password("wrong-password", hashed)
# Returns: False

Why Use Password Hashers?

Security Benefits:

  • Salted Hashing - Each password gets a unique salt

  • One-Way Function - Cannot reverse hash to get password

  • Slow Algorithms - Resistant to brute-force attacks

  • Industry Standard - Uses proven algorithms (bcrypt, PBKDF2)

Never Do This:

Store passwords in plain text

Use simple hashing (MD5, SHA1) without salt

Use fast hashing algorithms


Built-In Hashers

Ravyn provides password hashers via passlib:

from ravyn.contrib.auth.hashers import (
    make_password,
    check_password,
    BcryptPasswordHasher,
    PBKDF2PasswordHasher,
    PBKDF2SHA1PasswordHasher
)

Default Hasher: Bcrypt

from ravyn.contrib.auth.hashers import make_password

# Uses bcrypt by default
hashed = make_password("my-password")
print(hashed)
# Output: $bcrypt$2b$12$abcd1234...

Using Password Hashers

Hash a Password

from ravyn.contrib.auth.hashers import make_password

# Hash password
hashed_password = make_password("user-password-123")

# Store in database
user.password = hashed_password
await user.save()

Verify a Password

from ravyn.contrib.auth.hashers import check_password

# Get user from database
user = await User.get(email="user@example.com")

# Check password
if check_password("user-input-password", user.password):
    print("Password correct!")
else:
    print("Password incorrect!")

Complete Login Example

from ravyn import post
from ravyn.contrib.auth.hashers import check_password
from ravyn.exceptions import NotAuthorized

@post("/login")
async def login(email: str, password: str) -> dict:
    # Get user from database
    user = await User.get_or_none(email=email)

    if not user:
        raise NotAuthorized("Invalid credentials")

    # Verify password
    if not check_password(password, user.password):
        raise NotAuthorized("Invalid credentials")

    # Generate token or session
    return {"token": "abc123", "user_id": user.id}

Registration Example

from ravyn import post
from ravyn.contrib.auth.hashers import make_password

@post("/register")
async def register(email: str, password: str, name: str) -> dict:
    # Hash password
    hashed_password = make_password(password)

    # Create user
    user = await User.create(
        email=email,
        password=hashed_password,
        name=name
    )

    return {"user_id": user.id, "email": user.email}

Configuring Hashers

Via Settings

from ravyn import RavynSettings

class Settings(RavynSettings):
    @property
    def password_hashers(self) -> list[str]:
        return [
            "ravyn.contrib.auth.hashers.BcryptPasswordHasher",
            "ravyn.contrib.auth.hashers.PBKDF2PasswordHasher",
        ]

app = Ravyn(settings_module=Settings)

Available Hashers

Hasher Algorithm Speed Security
BcryptPasswordHasher bcrypt Slow (good) ⭐⭐⭐⭐⭐
PBKDF2PasswordHasher PBKDF2-SHA256 Slow (good) ⭐⭐⭐⭐
PBKDF2SHA1PasswordHasher PBKDF2-SHA1 Slow (good) ⭐⭐⭐

Tip

Use BcryptPasswordHasher (default) for best security.


Creating Custom Hashers

Subclass BasePasswordHasher to create custom hashers:

from ravyn.contrib.auth.hashers import BasePasswordHasher
import hashlib

class CustomHasher(BasePasswordHasher):
    algorithm = "custom"

    def encode(self, password: str, salt: str) -> str:
        # Your hashing logic
        hash_value = hashlib.sha256(
            f"{salt}{password}".encode()
        ).hexdigest()
        return f"${self.algorithm}${salt}${hash_value}"

    def verify(self, password: str, encoded: str) -> bool:
        # Your verification logic
        algorithm, salt, hash_value = encoded.split("$")[1:]
        return self.encode(password, salt) == encoded

    def safe_summary(self, encoded: str) -> dict:
        algorithm, salt, hash_value = encoded.split("$")[1:]
        return {
            "algorithm": algorithm,
            "salt": salt[:8] + "...",
            "hash": hash_value[:8] + "..."
        }

Common Pitfalls & Fixes

Pitfall 1: Storing Plain Text Passwords

Problem: Passwords stored without hashing.

# NEVER DO THIS - Security vulnerability!
@post("/register")
async def register(email: str, password: str) -> dict:
    user = await User.create(
        email=email,
        password=password  # Plain text!
    )
    return {"user_id": user.id}

Solution: Always hash passwords:

# Correct - Hash before storing
from ravyn.contrib.auth.hashers import make_password

@post("/register")
async def register(email: str, password: str) -> dict:
    user = await User.create(
        email=email,
        password=make_password(password)  # Hashed!
    )
    return {"user_id": user.id}

Pitfall 2: Comparing Hashed Passwords Directly

Problem: Using == to compare passwords.

# Wrong - won't work
if user_input_password == user.password:
    # This will never be True!
    pass

Solution: Use check_password():

# Correct
from ravyn.contrib.auth.hashers import check_password

if check_password(user_input_password, user.password):
    # Correct verification
    pass

Pitfall 3: Revealing User Existence

Problem: Different error messages for invalid email vs password.

# Security issue - reveals if email exists
@post("/login")
async def login(email: str, password: str) -> dict:
    user = await User.get_or_none(email=email)

    if not user:
        raise NotAuthorized("Email not found")  # Reveals email doesn't exist

    if not check_password(password, user.password):
        raise NotAuthorized("Wrong password")  # Reveals email exists

Solution: Use same error message:

# Correct - same error for both cases
@post("/login")
async def login(email: str, password: str) -> dict:
    user = await User.get_or_none(email=email)

    if not user or not check_password(password, user.password):
        raise NotAuthorized("Invalid credentials")  # Same message

    return {"token": "..."}

Pitfall 4: Not Installing passlib

Problem: ModuleNotFoundError when using hashers.

# Error if passlib not installed
from ravyn.contrib.auth.hashers import make_password

Solution: Install passlib:

pip install passlib[bcrypt]

Best Practices

1. Use Strong Passwords

# Good - enforce password strength
from ravyn.exceptions import ValidationError

@post("/register")
async def register(email: str, password: str) -> dict:
    if len(password) < 8:
        raise ValidationError("Password must be at least 8 characters")

    if not any(c.isupper() for c in password):
        raise ValidationError("Password must contain uppercase letter")

    hashed = make_password(password)
    user = await User.create(email=email, password=hashed)
    return {"user_id": user.id}

2. Add Rate Limiting

# Good - prevent brute force
from ravyn import post
from ravyn.exceptions import TooManyRequests

login_attempts = {}

@post("/login")
async def login(email: str, password: str, request: Request) -> dict:
    client_ip = request.client.host

    # Check attempts
    if login_attempts.get(client_ip, 0) >= 5:
        raise TooManyRequests("Too many login attempts")

    user = await User.get_or_none(email=email)

    if not user or not check_password(password, user.password):
        login_attempts[client_ip] = login_attempts.get(client_ip, 0) + 1
        raise NotAuthorized("Invalid credentials")

    # Reset on success
    login_attempts.pop(client_ip, None)
    return {"token": "..."}

3. Use Timing-Safe Comparison

# Good - check_password uses timing-safe comparison
if check_password(user_input, stored_hash):
    # Safe from timing attacks
    pass

Integration with Edgy ORM

Ravyn works seamlessly with Edgy for user management:

from edgy import Database, Registry, Model, fields
from ravyn.contrib.auth.hashers import make_password, check_password

database = Database("postgresql://localhost/mydb")
registry = Registry(database=database)

class User(Model):
    email = fields.EmailField(max_length=100, unique=True)
    password = fields.CharField(max_length=255)
    name = fields.CharField(max_length=100)

    class Meta:
        registry = registry

    def set_password(self, raw_password: str):
        self.password = make_password(raw_password)

    def check_password(self, raw_password: str) -> bool:
        return check_password(raw_password, self.password)

# Usage
user = User(email="user@example.com", name="Alice")
user.set_password("my-secret-password")
await user.save()

# Verify
if user.check_password("my-secret-password"):
    print("Password correct!")

Next Steps

Now that you understand password hashers, explore: