fixes
This commit is contained in:
310
docs/logout-issues/REMEMBER-ME-FEATURE-REFERENCE.md
Normal file
310
docs/logout-issues/REMEMBER-ME-FEATURE-REFERENCE.md
Normal file
@@ -0,0 +1,310 @@
|
||||
# Remember Me Feature - Quick Reference
|
||||
|
||||
## Overview
|
||||
The "Keep me logged in" checkbox on the login page now controls JWT token expiry time:
|
||||
- **Unchecked (default):** 1 hour session
|
||||
- **Checked:** 20 days session
|
||||
|
||||
## How It Works
|
||||
|
||||
### Frontend Flow
|
||||
```
|
||||
User checks "Keep me logged in" checkbox
|
||||
↓
|
||||
SignInForm.tsx passes isChecked to login()
|
||||
↓
|
||||
authStore.login(email, password, rememberMe)
|
||||
↓
|
||||
POST /api/v1/auth/login/ with { email, password, remember_me: true }
|
||||
```
|
||||
|
||||
### Backend Flow
|
||||
```
|
||||
LoginSerializer validates remember_me field (default: false)
|
||||
↓
|
||||
LoginView extracts remember_me from validated_data
|
||||
↓
|
||||
generate_access_token(user, account, remember_me=True)
|
||||
↓
|
||||
get_access_token_expiry(remember_me=True) returns 20 days
|
||||
↓
|
||||
JWT token created with 20-day expiry
|
||||
↓
|
||||
Token sent to frontend in response
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Token Expiry Settings
|
||||
**File:** `backend/igny8_core/settings.py`
|
||||
|
||||
```python
|
||||
# Default expiry (remember me unchecked)
|
||||
JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1)
|
||||
|
||||
# Extended expiry (remember me checked)
|
||||
JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=20)
|
||||
|
||||
# Refresh token expiry (independent of remember me)
|
||||
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30)
|
||||
```
|
||||
|
||||
### To Change Expiry Times
|
||||
1. Edit `JWT_ACCESS_TOKEN_EXPIRY` for default session length
|
||||
2. Edit `JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME` for remember me session length
|
||||
3. Restart backend server
|
||||
4. No database migration needed (settings-only change)
|
||||
|
||||
## Token Payload
|
||||
|
||||
### Without Remember Me
|
||||
```json
|
||||
{
|
||||
"user_id": 123,
|
||||
"account_id": 456,
|
||||
"email": "user@example.com",
|
||||
"exp": 1704124800, // 1 hour from now
|
||||
"iat": 1704121200,
|
||||
"type": "access",
|
||||
"remember_me": false
|
||||
}
|
||||
```
|
||||
|
||||
### With Remember Me
|
||||
```json
|
||||
{
|
||||
"user_id": 123,
|
||||
"account_id": 456,
|
||||
"email": "user@example.com",
|
||||
"exp": 1705846800, // 20 days from now
|
||||
"iat": 1704121200,
|
||||
"type": "access",
|
||||
"remember_me": true
|
||||
}
|
||||
```
|
||||
|
||||
## User Experience
|
||||
|
||||
### Scenario 1: Quick Session (Unchecked)
|
||||
- User logs in without checking "Keep me logged in"
|
||||
- User works for 1 hour
|
||||
- After 1 hour, token expires
|
||||
- Next API call returns 401
|
||||
- Frontend attempts token refresh
|
||||
- If refresh succeeds, user stays logged in
|
||||
- If refresh fails (after 30 days), user sees login page
|
||||
|
||||
### Scenario 2: Extended Session (Checked)
|
||||
- User logs in with "Keep me logged in" checked
|
||||
- User works for 20 days
|
||||
- Token remains valid for 20 days
|
||||
- After 20 days, token expires
|
||||
- Next API call returns 401
|
||||
- Frontend attempts token refresh
|
||||
- If refresh succeeds, user stays logged in
|
||||
- If refresh fails (after 30 days), user sees login page
|
||||
|
||||
### Scenario 3: Mixed Usage
|
||||
- User logs in with remember me on Device A (20 days)
|
||||
- User logs in without remember me on Device B (1 hour)
|
||||
- Each device has independent tokens with different expiry
|
||||
- Device A stays logged in for 20 days
|
||||
- Device B expires after 1 hour (but can refresh for up to 30 days)
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Why 1 Hour Default?
|
||||
- Balance between UX and security
|
||||
- Short enough to limit exposure if token stolen
|
||||
- Long enough to avoid constant re-authentication
|
||||
- Refresh token (30 days) extends session without requiring re-login
|
||||
|
||||
### Why 20 Days for Remember Me?
|
||||
- User explicitly opted in for extended session
|
||||
- Still expires eventually (not permanent)
|
||||
- Refresh token expiry (30 days) provides hard limit
|
||||
- Common pattern for "remember me" features
|
||||
|
||||
### Token Storage
|
||||
- Access token stored in localStorage (XSS risk mitigation via CSP)
|
||||
- Refresh token stored in localStorage
|
||||
- HttpOnly session cookie used for fallback authentication
|
||||
- All sensitive API calls require valid access token
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Test Steps
|
||||
1. **Test Default (1 hour):**
|
||||
```
|
||||
1. Login WITHOUT checking "Keep me logged in"
|
||||
2. Open browser dev tools → Application → Local Storage
|
||||
3. Find 'auth-storage' key
|
||||
4. Copy token value
|
||||
5. Go to jwt.io and decode
|
||||
6. Verify exp is ~1 hour from now
|
||||
7. Verify remember_me: false
|
||||
```
|
||||
|
||||
2. **Test Remember Me (20 days):**
|
||||
```
|
||||
1. Login WITH "Keep me logged in" checked
|
||||
2. Open browser dev tools → Application → Local Storage
|
||||
3. Find 'auth-storage' key
|
||||
4. Copy token value
|
||||
5. Go to jwt.io and decode
|
||||
6. Verify exp is ~20 days from now
|
||||
7. Verify remember_me: true
|
||||
```
|
||||
|
||||
3. **Test Expiry Behavior:**
|
||||
```
|
||||
1. Login with remember me
|
||||
2. Wait for access token to expire (or manually change exp in localStorage)
|
||||
3. Make API call
|
||||
4. Verify 401 received
|
||||
5. Verify token refresh attempted
|
||||
6. Verify new token received (if refresh token valid)
|
||||
7. Verify original request retried successfully
|
||||
```
|
||||
|
||||
### Automated Test (Backend)
|
||||
```python
|
||||
# backend/igny8_core/auth/tests/test_remember_me.py
|
||||
from django.test import TestCase
|
||||
from igny8_core.auth.utils import generate_access_token, decode_token
|
||||
from igny8_core.auth.models import User, Account
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
class RememberMeTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.account = Account.objects.create(name="Test Account")
|
||||
self.user = User.objects.create_user(
|
||||
email="test@example.com",
|
||||
password="testpass",
|
||||
account=self.account
|
||||
)
|
||||
|
||||
def test_default_expiry(self):
|
||||
"""Test that default token expires in 1 hour"""
|
||||
token = generate_access_token(self.user, self.account, remember_me=False)
|
||||
payload = decode_token(token)
|
||||
|
||||
exp_time = timezone.datetime.fromtimestamp(payload['exp'], tz=timezone.utc)
|
||||
now = timezone.now()
|
||||
expiry_delta = exp_time - now
|
||||
|
||||
# Should be ~1 hour (allow 5 minute variance)
|
||||
self.assertLess(expiry_delta, timedelta(hours=1, minutes=5))
|
||||
self.assertGreater(expiry_delta, timedelta(minutes=55))
|
||||
self.assertEqual(payload['remember_me'], False)
|
||||
|
||||
def test_remember_me_expiry(self):
|
||||
"""Test that remember me token expires in 20 days"""
|
||||
token = generate_access_token(self.user, self.account, remember_me=True)
|
||||
payload = decode_token(token)
|
||||
|
||||
exp_time = timezone.datetime.fromtimestamp(payload['exp'], tz=timezone.utc)
|
||||
now = timezone.now()
|
||||
expiry_delta = exp_time - now
|
||||
|
||||
# Should be ~20 days (allow 1 hour variance)
|
||||
self.assertLess(expiry_delta, timedelta(days=20, hours=1))
|
||||
self.assertGreater(expiry_delta, timedelta(days=19, hours=23))
|
||||
self.assertEqual(payload['remember_me'], True)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Token expires too quickly
|
||||
**Check:**
|
||||
1. Verify `JWT_ACCESS_TOKEN_EXPIRY` in settings.py
|
||||
2. Check if remember_me is being passed correctly
|
||||
3. Decode token at jwt.io and check exp field
|
||||
4. Check browser clock sync (wrong time can cause early expiry)
|
||||
|
||||
### Issue: Remember me checkbox doesn't work
|
||||
**Check:**
|
||||
1. Browser dev tools → Network tab → login request
|
||||
2. Verify request payload includes `remember_me: true`
|
||||
3. Check response token and decode at jwt.io
|
||||
4. Verify `remember_me: true` in token payload
|
||||
5. Check backend logs for any errors
|
||||
|
||||
### Issue: Token valid but still logged out
|
||||
**Check:**
|
||||
1. Middleware might be denying request (account/plan validation)
|
||||
2. Check browser console for logout_reason
|
||||
3. Check backend logs for AUTO-LOGOUT messages
|
||||
4. Verify account status and plan status in database
|
||||
|
||||
### Issue: Token refresh not working
|
||||
**Check:**
|
||||
1. Verify refresh token exists in localStorage
|
||||
2. Check refresh token expiry (30 days)
|
||||
3. Check /api/v1/auth/refresh/ endpoint response
|
||||
4. Verify new access token being stored
|
||||
5. Check for CORS issues if using different domain
|
||||
|
||||
## API Reference
|
||||
|
||||
### POST /api/v1/auth/login/
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"password": "password123",
|
||||
"remember_me": true // Optional, default: false
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"user": {
|
||||
"id": 123,
|
||||
"email": "user@example.com",
|
||||
"username": "user",
|
||||
"role": "owner",
|
||||
"account": { ... },
|
||||
...
|
||||
},
|
||||
"tokens": {
|
||||
"access": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"refresh": "eyJ0eXAiOiJKV1QiLCJhbGc...",
|
||||
"access_expires_at": "2024-01-22T12:00:00Z",
|
||||
"refresh_expires_at": "2024-02-20T12:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Related Files
|
||||
|
||||
### Backend
|
||||
- `backend/igny8_core/settings.py` - Token expiry settings
|
||||
- `backend/igny8_core/auth/utils.py` - Token generation logic
|
||||
- `backend/igny8_core/auth/serializers.py` - LoginSerializer with remember_me field
|
||||
- `backend/igny8_core/auth/urls.py` - LoginView that handles remember_me
|
||||
|
||||
### Frontend
|
||||
- `frontend/src/store/authStore.ts` - Login function with rememberMe parameter
|
||||
- `frontend/src/components/auth/SignInForm.tsx` - Checkbox that passes state to login
|
||||
- `frontend/src/services/api.ts` - Token refresh logic
|
||||
|
||||
### Documentation
|
||||
- `AUTHENTICATION-HOLISTIC-REVAMP.md` - Complete overview of authentication changes
|
||||
- `LOGOUT-CAUSES-COMPLETE-REFERENCE.md` - All logout causes documented
|
||||
- `LOGOUT-TRACKING-IMPLEMENTATION.md` - Logout tracking system details
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
1. **Dynamic session cookie age:** Make SESSION_COOKIE_AGE match access token expiry
|
||||
2. **Device fingerprinting:** "Remember this device" feature
|
||||
3. **User preference:** Save user's default remember me preference
|
||||
4. **Admin control:** Allow admins to set max remember me duration per account
|
||||
5. **Activity-based expiry:** Extend token on activity (sliding expiration)
|
||||
6. **Sign out all devices:** Invalidate all refresh tokens for a user
|
||||
Reference in New Issue
Block a user