add refresh token
This commit is contained in:
530
TESTING_REFRESH_TOKEN.md
Normal file
530
TESTING_REFRESH_TOKEN.md
Normal file
@@ -0,0 +1,530 @@
|
||||
# Testing Refresh Token System
|
||||
|
||||
This guide provides step-by-step instructions to test the refresh token implementation.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Database is running and migrations are applied:
|
||||
```bash
|
||||
npm run migration:run
|
||||
```
|
||||
|
||||
2. Application is running:
|
||||
```bash
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
3. Seed data is loaded (includes admin user):
|
||||
```bash
|
||||
npm run seed
|
||||
```
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### 1. Test User Login (Get Tokens)
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@retailpos.com",
|
||||
"password": "Admin123!"
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_token": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6...",
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"email": "admin@retailpos.com",
|
||||
"name": "Admin User",
|
||||
"roles": ["admin"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-01-15T10:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ Response includes `access_token`
|
||||
- ✅ Response includes `refresh_token`
|
||||
- ✅ Response includes user details
|
||||
- ✅ HTTP status is 200
|
||||
|
||||
**Save the tokens**:
|
||||
```bash
|
||||
# Save for next tests
|
||||
export ACCESS_TOKEN="<your-access-token>"
|
||||
export REFRESH_TOKEN="<your-refresh-token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Test Access Token Works
|
||||
|
||||
Use the access token to access a protected endpoint:
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -X GET http://localhost:3000/api/auth/profile \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"id": "uuid",
|
||||
"email": "admin@retailpos.com",
|
||||
"roles": ["admin"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ Protected endpoint returns user data
|
||||
- ✅ HTTP status is 200
|
||||
- ❌ Without token returns 401
|
||||
|
||||
---
|
||||
|
||||
### 3. Test Token Refresh
|
||||
|
||||
Exchange refresh token for new tokens:
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"refreshToken\": \"$REFRESH_TOKEN\"
|
||||
}"
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refresh_token": "x1y2z3a4b5c6d7e8f9g0h1i2j3k4l5m6n7o8p9q0r1s2t3u4v5w6...",
|
||||
"user": {
|
||||
"id": "uuid",
|
||||
"email": "admin@retailpos.com",
|
||||
"name": "Admin User",
|
||||
"roles": ["admin"],
|
||||
"isActive": true,
|
||||
"createdAt": "2025-01-15T10:00:00.000Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ New `access_token` is different from old one
|
||||
- ✅ New `refresh_token` is different from old one
|
||||
- ✅ HTTP status is 200
|
||||
- ✅ User details are returned
|
||||
|
||||
**Save new tokens**:
|
||||
```bash
|
||||
export NEW_ACCESS_TOKEN="<new-access-token>"
|
||||
export NEW_REFRESH_TOKEN="<new-refresh-token>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Test Token Rotation (Old Token Should Be Revoked)
|
||||
|
||||
Try to use the OLD refresh token again:
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"refreshToken\": \"$REFRESH_TOKEN\"
|
||||
}"
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"statusCode": 401,
|
||||
"message": "Invalid refresh token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ HTTP status is 401
|
||||
- ✅ Error message indicates invalid token
|
||||
- ✅ Old token cannot be reused (token rotation works!)
|
||||
|
||||
---
|
||||
|
||||
### 5. Test Logout
|
||||
|
||||
Logout and revoke the refresh token:
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/logout \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"refreshToken\": \"$NEW_REFRESH_TOKEN\"
|
||||
}"
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Logged out successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ HTTP status is 200
|
||||
- ✅ Success message returned
|
||||
|
||||
---
|
||||
|
||||
### 6. Test Revoked Token Cannot Be Used
|
||||
|
||||
Try to use the revoked token:
|
||||
|
||||
**Request**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"refreshToken\": \"$NEW_REFRESH_TOKEN\"
|
||||
}"
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"statusCode": 401,
|
||||
"message": "Invalid refresh token"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ HTTP status is 401
|
||||
- ✅ Revoked token cannot be used
|
||||
|
||||
---
|
||||
|
||||
### 7. Test Revoke All Tokens
|
||||
|
||||
Login again and test revoking all tokens:
|
||||
|
||||
**Step 1 - Login**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"email": "admin@retailpos.com",
|
||||
"password": "Admin123!"
|
||||
}'
|
||||
```
|
||||
|
||||
Save the tokens:
|
||||
```bash
|
||||
export ACCESS_TOKEN="<access-token>"
|
||||
export REFRESH_TOKEN="<refresh-token>"
|
||||
```
|
||||
|
||||
**Step 2 - Revoke All Tokens**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/revoke-all \
|
||||
-H "Authorization: Bearer $ACCESS_TOKEN"
|
||||
```
|
||||
|
||||
**Expected Response**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "All refresh tokens revoked successfully"
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3 - Verify Token is Revoked**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"refreshToken\": \"$REFRESH_TOKEN\"
|
||||
}"
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ Revoke all returns success
|
||||
- ✅ All tokens are revoked
|
||||
- ✅ Cannot use any refresh token after revoke-all
|
||||
|
||||
---
|
||||
|
||||
### 8. Test Invalid Scenarios
|
||||
|
||||
#### 8.1 Invalid Refresh Token
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"refreshToken": "invalid-token-12345"
|
||||
}'
|
||||
```
|
||||
|
||||
**Expected**: 401 Unauthorized
|
||||
|
||||
#### 8.2 Missing Refresh Token
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}'
|
||||
```
|
||||
|
||||
**Expected**: 400 Bad Request (validation error)
|
||||
|
||||
#### 8.3 Expired Access Token
|
||||
Wait for access token to expire (24 hours by default) or manually set a short expiry:
|
||||
|
||||
**Expected**: 401 Unauthorized when using expired access token
|
||||
|
||||
---
|
||||
|
||||
### 9. Database Verification
|
||||
|
||||
Connect to the database and verify tokens are stored correctly:
|
||||
|
||||
```bash
|
||||
# Connect to PostgreSQL
|
||||
psql -h localhost -U postgres -d retail_pos
|
||||
|
||||
# Or for remote database
|
||||
psql -h pg-30ed1d6a-renolation.b.aivencloud.com -p 20912 -U avnadmin -d defaultdb
|
||||
```
|
||||
|
||||
**Query 1 - View Refresh Tokens**:
|
||||
```sql
|
||||
SELECT
|
||||
id,
|
||||
LEFT(token, 20) || '...' as token_preview,
|
||||
"userId",
|
||||
"expiresAt",
|
||||
"isRevoked",
|
||||
"createdAt"
|
||||
FROM refresh_tokens
|
||||
ORDER BY "createdAt" DESC
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
**Query 2 - Count Active Tokens**:
|
||||
```sql
|
||||
SELECT COUNT(*) as active_tokens
|
||||
FROM refresh_tokens
|
||||
WHERE "isRevoked" = false
|
||||
AND "expiresAt" > NOW();
|
||||
```
|
||||
|
||||
**Query 3 - Tokens Per User**:
|
||||
```sql
|
||||
SELECT
|
||||
u.email,
|
||||
COUNT(*) as token_count,
|
||||
SUM(CASE WHEN rt."isRevoked" = false THEN 1 ELSE 0 END) as active_count
|
||||
FROM refresh_tokens rt
|
||||
JOIN users u ON u.id = rt."userId"
|
||||
GROUP BY u.email;
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ Tokens are stored as hashed values
|
||||
- ✅ Revoked tokens have `isRevoked = true`
|
||||
- ✅ Tokens have proper expiration dates
|
||||
- ✅ Foreign key relationship to users exists
|
||||
|
||||
---
|
||||
|
||||
### 10. Load Testing (Optional)
|
||||
|
||||
Test multiple concurrent refresh operations:
|
||||
|
||||
```bash
|
||||
# Create a simple load test script
|
||||
cat > test_refresh.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Login first
|
||||
RESPONSE=$(curl -s -X POST http://localhost:3000/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"admin@retailpos.com","password":"Admin123!"}')
|
||||
|
||||
REFRESH_TOKEN=$(echo $RESPONSE | jq -r '.refresh_token')
|
||||
|
||||
# Perform 10 sequential refreshes
|
||||
for i in {1..10}; do
|
||||
echo "Refresh attempt $i"
|
||||
RESPONSE=$(curl -s -X POST http://localhost:3000/api/auth/refresh \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"refreshToken\":\"$REFRESH_TOKEN\"}")
|
||||
|
||||
REFRESH_TOKEN=$(echo $RESPONSE | jq -r '.refresh_token')
|
||||
|
||||
if [ "$REFRESH_TOKEN" == "null" ]; then
|
||||
echo "Failed at attempt $i"
|
||||
break
|
||||
fi
|
||||
done
|
||||
EOF
|
||||
|
||||
chmod +x test_refresh.sh
|
||||
./test_refresh.sh
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
- ✅ All refreshes succeed
|
||||
- ✅ Each refresh invalidates previous token
|
||||
- ✅ No database errors
|
||||
|
||||
---
|
||||
|
||||
## Postman/Insomnia Collection
|
||||
|
||||
For easier testing, import this collection:
|
||||
|
||||
### Login
|
||||
```
|
||||
POST http://localhost:3000/api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "admin@retailpos.com",
|
||||
"password": "Admin123!"
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Token
|
||||
```
|
||||
POST http://localhost:3000/api/auth/refresh
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "{{refresh_token}}"
|
||||
}
|
||||
```
|
||||
|
||||
### Get Profile
|
||||
```
|
||||
GET http://localhost:3000/api/auth/profile
|
||||
Authorization: Bearer {{access_token}}
|
||||
```
|
||||
|
||||
### Logout
|
||||
```
|
||||
POST http://localhost:3000/api/auth/logout
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "{{refresh_token}}"
|
||||
}
|
||||
```
|
||||
|
||||
### Revoke All Tokens
|
||||
```
|
||||
POST http://localhost:3000/api/auth/revoke-all
|
||||
Authorization: Bearer {{access_token}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Test Results Summary
|
||||
|
||||
| Test Case | Expected Result | Status |
|
||||
|-----------|----------------|--------|
|
||||
| Login | Returns access_token + refresh_token | ✅ |
|
||||
| Access protected endpoint | Returns user data | ✅ |
|
||||
| Refresh token | Returns new tokens | ✅ |
|
||||
| Token rotation | Old token becomes invalid | ✅ |
|
||||
| Logout | Token is revoked | ✅ |
|
||||
| Use revoked token | 401 Unauthorized | ✅ |
|
||||
| Revoke all tokens | All tokens revoked | ✅ |
|
||||
| Invalid token | 401 Unauthorized | ✅ |
|
||||
| Missing token | 400 Bad Request | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: "Cannot find module '@nestjs/schedule'"
|
||||
**Solution**: The TokenCleanupService is optional. The system works without it. See OPTIONAL_SETUP.md if you want to enable automatic cleanup.
|
||||
|
||||
### Issue: "Invalid refresh token" immediately after login
|
||||
**Possible Causes**:
|
||||
1. Token not being stored in database
|
||||
2. Hash mismatch
|
||||
3. Database connection issue
|
||||
|
||||
**Debug**:
|
||||
```sql
|
||||
-- Check if token was created
|
||||
SELECT * FROM refresh_tokens ORDER BY "createdAt" DESC LIMIT 1;
|
||||
```
|
||||
|
||||
### Issue: Tokens not being revoked
|
||||
**Debug**:
|
||||
```sql
|
||||
-- Check if isRevoked is being set
|
||||
SELECT "isRevoked", COUNT(*) FROM refresh_tokens GROUP BY "isRevoked";
|
||||
```
|
||||
|
||||
### Issue: Database bloat
|
||||
**Solution**: Run manual cleanup:
|
||||
```sql
|
||||
DELETE FROM refresh_tokens WHERE "expiresAt" < NOW();
|
||||
DELETE FROM refresh_tokens WHERE "isRevoked" = true;
|
||||
```
|
||||
|
||||
Or enable automatic cleanup (see OPTIONAL_SETUP.md).
|
||||
|
||||
---
|
||||
|
||||
## Production Testing Checklist
|
||||
|
||||
Before deploying to production:
|
||||
|
||||
- [ ] All test scenarios pass
|
||||
- [ ] Token rotation works correctly
|
||||
- [ ] Tokens are stored as hashes
|
||||
- [ ] Foreign key constraints work
|
||||
- [ ] Expired tokens are rejected
|
||||
- [ ] Revoked tokens are rejected
|
||||
- [ ] Rate limiting is configured (optional)
|
||||
- [ ] HTTPS is enforced
|
||||
- [ ] Logging is in place
|
||||
- [ ] Database backup is configured
|
||||
- [ ] Cleanup strategy is decided (automatic or manual)
|
||||
|
||||
---
|
||||
|
||||
## Performance Benchmarks
|
||||
|
||||
Expected performance (adjust based on your infrastructure):
|
||||
|
||||
- Login: < 200ms
|
||||
- Refresh: < 150ms
|
||||
- Logout: < 100ms
|
||||
- Token validation: < 50ms
|
||||
- Database query: < 20ms
|
||||
|
||||
Monitor these metrics in production and optimize as needed.
|
||||
Reference in New Issue
Block a user