swiftops-backend / docs /agent /implementation-notes /PASSWORD_RESET_TEST_GUIDE.md
kamau1's picture
feat(project): add complete project setup workflow with service methods and API endpoints for regions, roles, subcontractors, and finalization including validation and authorization
4835b24
# Password Reset Testing Guide
Complete guide for testing the password reset functionality with audit logging.
## Overview
The password reset system provides secure token-based password recovery with:
- Secure token generation (32+ characters)
- Time-limited tokens (1 hour expiry)
- Email delivery via Resend
- Comprehensive audit logging
- Protection against email enumeration attacks
- Single-use tokens
## Prerequisites
1. **Environment Variables** (see `.env.example`):
```bash
RESEND_API_KEY=re_xxxxx
RESEND_FROM_EMAIL=swiftops@atomio.tech
APP_DOMAIN=swiftops.atomio.tech
APP_PROTOCOL=https
PASSWORD_RESET_TOKEN_EXPIRY_HOURS=1
```
2. **Database Migrations**:
```bash
# Ensure user_invitations table exists
# (Created in migration 11_user_invitations.sql)
```
3. **Test User**:
```bash
# Register a test user first
curl -X POST http://localhost:8000/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "TestPass123!",
"first_name": "Test",
"last_name": "User"
}'
```
## Test Scenarios
### 1. Request Password Reset
**Endpoint**: `POST /api/v1/auth/forgot-password`
**Request**:
```bash
curl -X POST http://localhost:8000/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com"
}'
```
**Expected Response** (200 OK):
```json
{
"message": "If an account exists with this email, you will receive a password reset link."
}
```
**Verification**:
1. Check database for reset token:
```sql
SELECT token, expires_at, status
FROM user_invitations
WHERE email = 'test@example.com'
AND invitation_metadata->>'type' = 'password_reset'
ORDER BY created_at DESC
LIMIT 1;
```
2. Check audit log:
```sql
SELECT action, description, created_at
FROM audit_logs
WHERE action = 'password_reset_request'
ORDER BY created_at DESC
LIMIT 1;
```
3. Check email (if Resend configured):
- Subject: "Reset Your SwiftOps Password"
- Contains reset link with token
- Mentions 1 hour expiry
### 2. Reset Password with Valid Token
**Endpoint**: `POST /api/v1/auth/reset-password`
**Request**:
```bash
# Get token from database first
TOKEN="<token_from_database>"
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "'$TOKEN'",
"new_password": "NewTestPass456!"
}'
```
**Expected Response** (200 OK):
```json
{
"message": "Password reset successful. You can now login with your new password."
}
```
**Verification**:
1. Check token status updated:
```sql
SELECT status, accepted_at
FROM user_invitations
WHERE token = '<token>';
```
- Status should be 'accepted'
- accepted_at should be set
2. Check audit log:
```sql
SELECT action, description
FROM audit_logs
WHERE action = 'password_reset'
ORDER BY created_at DESC
LIMIT 1;
```
3. Login with new password:
```bash
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "NewTestPass456!"
}'
```
4. Verify old password fails:
```bash
curl -X POST http://localhost:8000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "test@example.com",
"password": "TestPass123!"
}'
```
- Should return 401 Unauthorized
### 3. Invalid Token
**Request**:
```bash
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "invalid-token-12345678901234567890",
"new_password": "NewTestPass456!"
}'
```
**Expected Response** (400 Bad Request):
```json
{
"detail": "Invalid or expired reset token"
}
```
**Verification**:
- Check audit log for failed attempt:
```sql
SELECT action, description, additional_metadata
FROM audit_logs
WHERE action = 'password_reset_failed'
ORDER BY created_at DESC
LIMIT 1;
```
### 4. Expired Token
**Setup**:
```sql
-- Manually expire a token for testing
UPDATE user_invitations
SET expires_at = NOW() - INTERVAL '1 hour'
WHERE email = 'test@example.com'
AND status = 'pending'
AND invitation_metadata->>'type' = 'password_reset';
```
**Request**:
```bash
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "<expired_token>",
"new_password": "NewTestPass456!"
}'
```
**Expected Response** (400 Bad Request):
```json
{
"detail": "Reset token has expired. Please request a new one."
}
```
**Verification**:
- Token status updated to 'expired':
```sql
SELECT status FROM user_invitations WHERE token = '<expired_token>';
```
### 5. Token Reuse Prevention
**Request**:
```bash
# Use the same token twice
TOKEN="<already_used_token>"
# First use (should succeed)
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "'$TOKEN'",
"new_password": "NewTestPass456!"
}'
# Second use (should fail)
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{
"token": "'$TOKEN'",
"new_password": "AnotherPass789!"
}'
```
**Expected**:
- First request: 200 OK
- Second request: 400 Bad Request
### 6. Non-Existent Email (No Enumeration)
**Request**:
```bash
curl -X POST http://localhost:8000/api/v1/auth/forgot-password \
-H "Content-Type: application/json" \
-d '{
"email": "nonexistent@example.com"
}'
```
**Expected Response** (200 OK):
```json
{
"message": "If an account exists with this email, you will receive a password reset link."
}
```
**Verification**:
- Same response as valid email (prevents enumeration)
- No token created in database
- Audit log still created
### 7. Password Validation
**Weak Passwords to Test**:
```bash
# Too short
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"token": "test", "new_password": "short"}'
# No uppercase
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"token": "test", "new_password": "nouppercase123"}'
# No digits
curl -X POST http://localhost:8000/api/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{"token": "test", "new_password": "NoDigitsHere"}'
```
**Expected**: All should return 422 Validation Error
## Automated Tests
### Python Tests
Run pytest tests:
```bash
# All password reset tests
pytest tests/integration/test_password_reset.py -v
# Specific test
pytest tests/integration/test_password_reset.py::TestPasswordReset::test_reset_password_with_valid_token -v
# All auth audit log tests
pytest tests/integration/test_auth_audit_logs.py -v
```
### JavaScript Tests
Run Node.js tests:
```bash
cd tests/integration
npm install axios # If not already installed
node test_password_reset.js
```
## Audit Log Verification
Check all audit logs for password reset flow:
```sql
-- All password reset related logs
SELECT
action,
description,
user_email,
ip_address,
created_at
FROM audit_logs
WHERE action IN (
'password_reset_request',
'password_reset',
'password_reset_failed'
)
ORDER BY created_at DESC
LIMIT 20;
-- Detailed view with metadata
SELECT
action,
description,
additional_metadata,
created_at
FROM audit_logs
WHERE entity_type = 'auth'
AND action LIKE '%password%'
ORDER BY created_at DESC;
```
## Email Template Testing
If Resend is configured, verify email appearance:
1. **Subject**: "Reset Your SwiftOps Password"
2. **Content**:
- Personalized greeting with user's name
- Clear call-to-action button
- Plain text link as fallback
- Security notice about 1-hour expiry
- Warning if user didn't request reset
3. **Styling**:
- Responsive design
- Professional appearance
- SwiftOps branding
## Security Checklist
- [ ] Tokens are cryptographically secure (32+ characters)
- [ ] Tokens expire after 1 hour
- [ ] Tokens are single-use only
- [ ] No email enumeration (same response for valid/invalid emails)
- [ ] Password strength requirements enforced
- [ ] All actions logged in audit trail
- [ ] IP addresses captured in audit logs
- [ ] User agents captured in audit logs
- [ ] Email delivery failures handled gracefully
- [ ] Database errors don't expose sensitive info
## Troubleshooting
### Email Not Received
1. Check Resend API key:
```bash
echo $RESEND_API_KEY
```
2. Check application logs:
```bash
grep "Password reset" logs/app.log
grep "Email send error" logs/app.log
```
3. Verify Resend domain configuration
### Token Not Working
1. Check token exists and is valid:
```sql
SELECT * FROM user_invitations
WHERE token = '<token>';
```
2. Check expiry time:
```sql
SELECT
token,
expires_at,
expires_at > NOW() as is_valid,
status
FROM user_invitations
WHERE token = '<token>';
```
### Audit Logs Not Created
1. Check database connection
2. Verify audit_logs table exists
3. Check application logs for errors
4. Ensure AuditService is imported correctly
## Performance Considerations
- Token generation: < 10ms
- Database query: < 50ms
- Email sending: < 2s (async)
- Total request time: < 100ms (excluding email)
## Next Steps
After testing password reset:
1. Test all auth endpoints with audit logging
2. Verify audit log retention policies
3. Set up monitoring for failed reset attempts
4. Configure email rate limiting
5. Review security policies with team