Spaces:
Sleeping
Sleeping
| # 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 | |