Spaces:
Runtime error
Runtime error
| # Authentication & API Security - Testing Guide | |
| **Feature**: Authentication & API Security (Spec 001) | |
| **Date**: 2026-01-09 | |
| **Status**: Ready for Testing | |
| ## Prerequisites | |
| Before testing, ensure: | |
| 1. **Backend is running**: | |
| ```bash | |
| cd backend | |
| python -m uvicorn src.main:app --reload | |
| # Should be running at http://localhost:8000 | |
| ``` | |
| 2. **Database migrations applied**: | |
| ```bash | |
| cd backend | |
| python -m alembic upgrade head | |
| ``` | |
| 3. **Frontend is running**: | |
| ```bash | |
| cd frontend | |
| npm run dev | |
| # Should be running at http://localhost:3000 | |
| ``` | |
| 4. **Environment variables configured**: | |
| - `backend/.env` has `BETTER_AUTH_SECRET` | |
| - `frontend/.env.local` has same `BETTER_AUTH_SECRET` | |
| - Both secrets match exactly | |
| ## Test Suite | |
| ### T048: Test Signup Flow End-to-End | |
| **Objective**: Verify new users can create accounts and data is stored correctly in database | |
| **Steps**: | |
| 1. **Navigate to signup page**: | |
| - Open browser to `http://localhost:3000/auth/signup` | |
| - Verify signup form is displayed with email, password, and name fields | |
| 2. **Test validation errors**: | |
| - Try submitting with empty fields → Should show validation errors | |
| - Try weak password (e.g., "pass") → Should show "Password must be at least 8 characters" | |
| - Try invalid email (e.g., "notanemail") → Should show email format error | |
| 3. **Create valid account**: | |
| - Email: `test1@example.com` | |
| - Password: `SecurePass123` | |
| - Name: `Test User 1` | |
| - Click "Sign Up" | |
| - **Expected**: Success message, redirect to signin page | |
| 4. **Verify in database**: | |
| ```bash | |
| # Connect to your database and run: | |
| SELECT id, email, name, password_hash, created_at FROM users WHERE email = 'test1@example.com'; | |
| ``` | |
| - **Expected**: User record exists | |
| - **Expected**: `password_hash` is bcrypt hash (starts with `$2b$`) | |
| - **Expected**: `created_at` timestamp is recent | |
| 5. **Test duplicate email**: | |
| - Try signing up again with `test1@example.com` | |
| - **Expected**: 409 Conflict error "Email already registered" | |
| **Pass Criteria**: | |
| - ✅ Form validation works correctly | |
| - ✅ Valid signup creates user in database | |
| - ✅ Password is hashed (not stored in plain text) | |
| - ✅ Duplicate email is rejected with 409 error | |
| - ✅ User is redirected to signin after successful signup | |
| --- | |
| ### T049: Test Signin Flow End-to-End | |
| **Objective**: Verify users can authenticate and receive valid JWT tokens | |
| **Steps**: | |
| 1. **Navigate to signin page**: | |
| - Open browser to `http://localhost:3000/auth/signin` | |
| - Verify signin form is displayed | |
| 2. **Test invalid credentials**: | |
| - Email: `test1@example.com` | |
| - Password: `WrongPassword123` | |
| - Click "Sign In" | |
| - **Expected**: 401 error "Invalid email or password" | |
| 3. **Test valid credentials**: | |
| - Email: `test1@example.com` | |
| - Password: `SecurePass123` | |
| - Click "Sign In" | |
| - **Expected**: Success, redirect to home page (`/`) | |
| 4. **Verify JWT token**: | |
| - Open browser DevTools → Application → Local Storage → `http://localhost:3000` | |
| - Find `auth_session` key | |
| - **Expected**: JSON object with `token` and `user` fields | |
| - Copy the token value | |
| 5. **Decode JWT token** (use jwt.io or command line): | |
| ```bash | |
| # Using Python | |
| python -c "import jwt; print(jwt.decode('YOUR_TOKEN_HERE', options={'verify_signature': False}))" | |
| ``` | |
| - **Expected payload**: | |
| ```json | |
| { | |
| "sub": "1", // User ID | |
| "email": "test1@example.com", | |
| "iat": 1704067200, // Issued at timestamp | |
| "exp": 1704672000, // Expiration (7 days later) | |
| "iss": "better-auth" | |
| } | |
| ``` | |
| 6. **Verify session persistence**: | |
| - Refresh the page | |
| - **Expected**: Still logged in (no redirect to signin) | |
| - **Expected**: User name displayed in header | |
| 7. **Test signout**: | |
| - Click "Sign Out" button in header | |
| - **Expected**: Redirect to signin page | |
| - **Expected**: localStorage `auth_session` is cleared | |
| **Pass Criteria**: | |
| - ✅ Invalid credentials return 401 error | |
| - ✅ Valid credentials return JWT token | |
| - ✅ Token contains correct user_id, email, and expiration | |
| - ✅ Token expiration is 7 days from issuance | |
| - ✅ Session persists across page refreshes | |
| - ✅ Signout clears session and redirects | |
| --- | |
| ### T050: Test Protected API Access | |
| **Objective**: Verify API endpoints require valid JWT tokens and reject invalid tokens | |
| **Steps**: | |
| 1. **Test unauthenticated request**: | |
| ```bash | |
| # Try to fetch tasks without token | |
| curl http://localhost:8000/api/tasks | |
| ``` | |
| - **Expected**: 401 Unauthorized | |
| - **Expected**: Response body: `{"detail": "Not authenticated"}` | |
| 2. **Test with valid token**: | |
| - Sign in to get a valid token (from T049) | |
| - Copy token from localStorage | |
| ```bash | |
| # Replace YOUR_TOKEN with actual token | |
| curl http://localhost:8000/api/tasks \ | |
| -H "Authorization: Bearer YOUR_TOKEN" | |
| ``` | |
| - **Expected**: 200 OK | |
| - **Expected**: Returns task list (may be empty) | |
| 3. **Test with invalid token**: | |
| ```bash | |
| curl http://localhost:8000/api/tasks \ | |
| -H "Authorization: Bearer invalid_token_here" | |
| ``` | |
| - **Expected**: 401 Unauthorized | |
| - **Expected**: Error message about invalid token | |
| 4. **Test with expired token**: | |
| - Manually create an expired token or wait 7 days (not practical) | |
| - Alternative: Temporarily change `JWT_EXPIRATION_DAYS=0` in backend/.env, restart backend, get new token, wait 1 minute | |
| ```bash | |
| curl http://localhost:8000/api/tasks \ | |
| -H "Authorization: Bearer EXPIRED_TOKEN" | |
| ``` | |
| - **Expected**: 401 Unauthorized | |
| - **Expected**: Error code `TOKEN_EXPIRED` | |
| 5. **Test frontend automatic redirect**: | |
| - In browser, manually edit localStorage to set invalid token | |
| - Try to access home page (`/`) | |
| - **Expected**: Automatic redirect to `/auth/signin` | |
| 6. **Test all protected endpoints**: | |
| - With valid token, test: | |
| - `GET /api/tasks` → 200 OK | |
| - `POST /api/tasks` → 201 Created | |
| - `GET /api/tasks/{id}` → 200 OK | |
| - `PATCH /api/tasks/{id}` → 200 OK | |
| - `DELETE /api/tasks/{id}` → 204 No Content | |
| - `GET /api/auth/me` → 200 OK | |
| **Pass Criteria**: | |
| - ✅ Requests without token return 401 | |
| - ✅ Requests with valid token succeed | |
| - ✅ Requests with invalid token return 401 | |
| - ✅ Requests with expired token return 401 with TOKEN_EXPIRED | |
| - ✅ Frontend automatically redirects on 401 | |
| - ✅ All task endpoints require authentication | |
| --- | |
| ### T051: Test User Data Isolation | |
| **Objective**: Verify users can only access their own tasks, not other users' tasks | |
| **Steps**: | |
| 1. **Create two user accounts**: | |
| - User A: `usera@example.com` / `PasswordA123` | |
| - User B: `userb@example.com` / `PasswordB123` | |
| 2. **Sign in as User A**: | |
| - Navigate to `/auth/signin` | |
| - Sign in with User A credentials | |
| - Copy User A's JWT token from localStorage | |
| 3. **Create tasks as User A**: | |
| - Create 2-3 tasks through the UI | |
| - Note the task IDs (check Network tab or database) | |
| 4. **Sign out and sign in as User B**: | |
| - Click "Sign Out" | |
| - Sign in with User B credentials | |
| - Copy User B's JWT token | |
| 5. **Create tasks as User B**: | |
| - Create 2-3 different tasks through the UI | |
| 6. **Verify User B cannot see User A's tasks**: | |
| - Check the task list in UI | |
| - **Expected**: Only User B's tasks are visible | |
| - **Expected**: User A's tasks are NOT visible | |
| 7. **Test API-level isolation**: | |
| ```bash | |
| # Get User A's task ID from database | |
| # Try to access it with User B's token | |
| curl http://localhost:8000/api/tasks/USER_A_TASK_ID \ | |
| -H "Authorization: Bearer USER_B_TOKEN" | |
| ``` | |
| - **Expected**: 404 Not Found (task doesn't exist for User B) | |
| 8. **Test cross-user modification attempt**: | |
| ```bash | |
| # Try to update User A's task with User B's token | |
| curl -X PATCH http://localhost:8000/api/tasks/USER_A_TASK_ID \ | |
| -H "Authorization: Bearer USER_B_TOKEN" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"completed": true}' | |
| ``` | |
| - **Expected**: 404 Not Found | |
| 9. **Test cross-user deletion attempt**: | |
| ```bash | |
| # Try to delete User A's task with User B's token | |
| curl -X DELETE http://localhost:8000/api/tasks/USER_A_TASK_ID \ | |
| -H "Authorization: Bearer USER_B_TOKEN" | |
| ``` | |
| - **Expected**: 404 Not Found | |
| 10. **Verify in database**: | |
| ```sql | |
| -- Check that tasks are correctly associated with users | |
| SELECT id, user_id, title FROM tasks ORDER BY user_id, id; | |
| ``` | |
| - **Expected**: User A's tasks have `user_id = 1` | |
| - **Expected**: User B's tasks have `user_id = 2` | |
| - **Expected**: No cross-contamination | |
| **Pass Criteria**: | |
| - ✅ User A can only see their own tasks | |
| - ✅ User B can only see their own tasks | |
| - ✅ User B cannot access User A's tasks via API (404) | |
| - ✅ User B cannot modify User A's tasks (404) | |
| - ✅ User B cannot delete User A's tasks (404) | |
| - ✅ Database correctly associates tasks with user_id | |
| - ✅ All queries are filtered by authenticated user | |
| --- | |
| ## Test Results Summary | |
| After completing all tests, fill in the results: | |
| | Test | Status | Notes | | |
| |------|--------|-------| | |
| | T048: Signup Flow | ⬜ Pass / ⬜ Fail | | | |
| | T049: Signin Flow | ⬜ Pass / ⬜ Fail | | | |
| | T050: Protected API | ⬜ Pass / ⬜ Fail | | | |
| | T051: User Isolation | ⬜ Pass / ⬜ Fail | | | |
| ## Common Issues & Troubleshooting | |
| ### Issue: "BETTER_AUTH_SECRET not found" | |
| - **Cause**: Environment variable not set | |
| - **Fix**: Ensure both `backend/.env` and `frontend/.env.local` have `BETTER_AUTH_SECRET` | |
| - **Verify**: Secrets must be identical in both files | |
| ### Issue: "Token signature verification failed" | |
| - **Cause**: Frontend and backend have different secrets | |
| - **Fix**: Copy exact same secret to both .env files | |
| - **Verify**: Run `grep BETTER_AUTH_SECRET backend/.env frontend/.env.local` | |
| ### Issue: "401 Unauthorized" on all requests | |
| - **Cause**: Token not being sent or invalid | |
| - **Fix**: Check localStorage has valid token, check Authorization header in Network tab | |
| ### Issue: "User can see other users' tasks" | |
| - **Cause**: Missing user_id filter in queries | |
| - **Fix**: Verify `get_current_user` dependency is applied to all task endpoints | |
| - **Check**: `backend/src/api/routes/tasks.py` should use `current_user_id = Depends(get_current_user)` | |
| ### Issue: Database migration errors | |
| - **Cause**: Migration not applied or database out of sync | |
| - **Fix**: Run `python -m alembic upgrade head` in backend directory | |
| ## Security Checklist | |
| After testing, verify: | |
| - [ ] Passwords are hashed (never stored in plain text) | |
| - [ ] JWT tokens expire after 7 days | |
| - [ ] Invalid tokens are rejected with 401 | |
| - [ ] Expired tokens are rejected with 401 | |
| - [ ] Users cannot access other users' data | |
| - [ ] All task endpoints require authentication | |
| - [ ] BETTER_AUTH_SECRET is not committed to git | |
| - [ ] Error messages don't leak sensitive information | |
| - [ ] Token signature is verified on every request | |
| ## Next Steps | |
| Once all tests pass: | |
| 1. Mark tasks T048-T051 as complete in `tasks.md` | |
| 2. Create git commit with authentication implementation | |
| 3. Consider additional security enhancements: | |
| - Rate limiting on auth endpoints | |
| - Account lockout after failed attempts | |
| - Password reset functionality | |
| - Refresh token mechanism | |
| - Multi-factor authentication (MFA) | |