311 lines
9.3 KiB
Markdown
311 lines
9.3 KiB
Markdown
# 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
|