Files
igny8/docs/logout-issues/REMEMBER-ME-FEATURE-REFERENCE.md
IGNY8 VPS (Salman) 4bba5a9a1f fixes
2025-12-17 04:55:49 +00:00

9.3 KiB

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

# 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

{
  "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

{
  "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)

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

{
  "email": "user@example.com",
  "password": "password123",
  "remember_me": true  // Optional, default: false
}

Response:

{
  "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"
    }
  }
}

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