# 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="" 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 = ''; ``` - 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": "", "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 = ''; ``` ### 5. Token Reuse Prevention **Request**: ```bash # Use the same token twice 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 = ''; ``` 2. Check expiry time: ```sql SELECT token, expires_at, expires_at > NOW() as is_valid, status FROM user_invitations WHERE 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