# Implementing Local Auth

> User registration, login, and password hashing with bcrypt for local authentication flows.

- Skill: `7a336e6e/implementing-local-auth` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add 7a336e6e/implementing-local-auth`
- Raw SKILL.md: https://api.skillmd.com/api/skills/7a336e6e/implementing-local-auth/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: 7a336e6e (https://skillmd.com/u/7a336e6e)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/7a336e6e/implementing-local-auth

---


# Implementing Local Auth

## Goal

Implement a secure local authentication system with user registration, login, and password hashing using bcrypt. The system must validate inputs, enforce password policy, and return JWTs on successful authentication.

## When to Use

- Building a new application that needs email/password authentication.
- Adding local auth alongside existing OAuth providers.
- Replacing an insecure or legacy authentication system.
- Any time users need to create accounts and log in with credentials they control.

## Instructions

### Registration Flow

The registration flow follows these steps: validate input, check uniqueness, hash password, create user, return token.

```python
import bcrypt
from flask import Blueprint, request, jsonify
from marshmallow import Schema, fields, validate, ValidationError

auth_bp = Blueprint("auth", __name__, url_prefix="/api/v1/auth")


class RegisterSchema(Schema):
    email = fields.Email(required=True)
    password = fields.Str(
        required=True,
        validate=validate.Length(min=8),
    )
    name = fields.Str(required=True, validate=validate.Length(min=1, max=255))


def hash_password(plain: str) -> str:
    """Hash a password with bcrypt using a work factor of 12."""
    salt = bcrypt.gensalt(rounds=12)
    return bcrypt.hashpw(plain.encode("utf-8"), salt).decode("utf-8")


@auth_bp.route("/register", methods=["POST"])
def register():
    schema = RegisterSchema()
    try:
        data = schema.load(request.get_json())
    except ValidationError as err:
        return jsonify({"error": "Validation failed", "details": err.messages}), 400

    # Check uniqueness
    existing = User.query.filter_by(email=data["email"]).first()
    if existing:
        # Return generic message to avoid revealing registered emails
        return jsonify({"error": "Registration failed. Please try again."}), 400

    # Hash password and create user
    hashed = hash_password(data["password"])
    user = User(email=data["email"], password_hash=hashed, name=data["name"])
    db.session.add(user)
    db.session.commit()

    token = generate_access_token(user)
    return jsonify({"access_token": token}), 201
```

### Login Flow

The login flow follows these steps: find user by email, verify password, generate token, return token.

```python
class LoginSchema(Schema):
    email = fields.Email(required=True)
    password = fields.Str(required=True)


def verify_password(plain: str, hashed: str) -> bool:
    """Verify a plaintext password against a bcrypt hash."""
    return bcrypt.checkpw(plain.encode("utf-8"), hashed.encode("utf-8"))


@auth_bp.route("/login", methods=["POST"])
def login():
    schema = LoginSchema()
    try:
        data = schema.load(request.get_json())
    except ValidationError as err:
        return jsonify({"error": "Invalid credentials"}), 401

    user = User.query.filter_by(email=data["email"]).first()
    if not user or not verify_password(data["password"], user.password_hash):
        return jsonify({"error": "Invalid credentials"}), 401

    token = generate_access_token(user)
    return jsonify({"access_token": token}), 200
```

### Input Validation

- **Email format**: Use a schema validator to enforce RFC-compliant email addresses.
- **Password strength**: Minimum 8 characters with complexity requirements. See [password-policy.md](references/password-policy.md) for full policy.
- **Name**: Non-empty string, max 255 characters.

### Password Hashing with bcrypt

Always use bcrypt with a work factor of 12. This provides a good balance between security and performance. The work factor doubles the computation for each increment, so 12 results in 2^12 (4096) iterations of the key derivation.

```python
# Work factor 12 is the recommended default
salt = bcrypt.gensalt(rounds=12)
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
```

## Constraints

### ✅ Do
- Hash passwords with bcrypt using a work factor of 12.
- Validate email format before processing registration.
- Rate limit registration endpoints to prevent abuse.
- Return consistent, generic error messages for both login and registration failures.
- Enforce password complexity requirements as defined in the password policy.
- Store only the bcrypt hash, never the plaintext password.


### ❌ Don&#x27;t
- Store plaintext passwords under any circumstances.
- Reveal whether an email address is already registered on login failure.
- Use MD5 or SHA family hashes for password storage.
- Allow unlimited registration attempts from a single IP.
- Return different error messages for "user not found" vs "wrong password".
- Log plaintext passwords in any application or server logs.


## Output Format

**Registration success** (201):
```json
{ "access_token": "<jwt>" }
```

**Login success** (200):
```json
{ "access_token": "<jwt>" }
```

**Validation error** (400):
```json
{ "error": "Validation failed", "details": { "email": ["Not a valid email address."] } }
```

**Auth failure** (401):
```json
{ "error": "Invalid credentials" }
```

## Dependencies

- [Building API Routes](../../backend/building-api-routes/SKILL.md) -- route structure and request handling.
- [Designing Schemas](../../database/designing-schemas/SKILL.md) -- user table schema design.
- [Password Policy](references/password-policy.md) -- password strength and lockout rules.

