Skip to content

Runbook: Sign-in fails with "invalid credentials"

Symptom

Attempting to sign in as local@example.com (or any seeded user) returns "Invalid credentials" despite using the correct password. The sign-in E2E test fails at the POST /auth/login step.

Likely cause

The user's password column in Postgres contains a truncated or corrupted bcrypt hash. This happens when the seed script runs with an incompatible bcrypt version or when a partial migration corrupts the row. The telltale sign: the password field is shorter than the expected 60 characters (a valid bcrypt hash is always exactly 60 chars: $2b$10$...).

Diagnosis

# Check the password hash length
sudo docker exec ebit-db psql -U ebit -d ebit -c \
  "SELECT email, length(password) as len, substring(password, 1, 10) as prefix FROM \"User\" WHERE email = 'local@example.com';"

# Expected output for a healthy hash:
#         email          | len |   prefix
# -----------------------+-----+------------
#  local@example.com     |  60 | $2b$10$xxx

# If len < 60 (especially len = 2), the hash is corrupted.

Fix

Option 1: Re-seed the database

cd ebit-api
npm run db:reset    # drops + recreates + seeds
# This sets DEBUG_SEED_LOCAL=true which creates local@example.com:password

Option 2: Patch the password directly

# Generate a fresh bcrypt hash for "password" (cost factor 10)
node -e "const bcrypt = require('bcryptjs'); bcrypt.hash('password', 10, (e, h) => console.log(h))"
# Output: $2a$10$<hash>

# Update the user's password
sudo docker exec ebit-db psql -U ebit -d ebit -c \
  "UPDATE \"User\" SET password = '\$2a\$10\$<paste_full_hash_here>' WHERE email = 'local@example.com';"

Option 3: Clear login-attempt lockout

If the password hash is fine but you're locked out from too many failed attempts:

# Check for login-attempt rate-limit keys in Redis
redis-cli -a cache KEYS "*login*local@example.com*"

# Delete the lockout key
redis-cli -a cache DEL "user:login-attempts:local@example.com"

Verification

# Test sign-in via curl
curl -s -X POST http://localhost:4000/auth/login \
  -H "Content-Type: application/json" \
  -H "x-captcha-token: pass" \
  -d '{"email":"local@example.com","password":"password"}' \
  | python3 -c "import json,sys; d=json.load(sys.stdin); print('OK' if 'accessToken' in d else f'FAIL: {d}')"

Prevention

  • Always use npm run db:seed (not manual INSERTs) to create test users — the seed script uses the project's bcrypt configuration
  • If switching Node.js versions, re-run npm install to rebuild native bcrypt bindings
  • The DEBUG_SEED_LOCAL=true env var in docker-compose.yml:132 ensures the demo user is created with every fresh seed