AIDA / docs /AUTH_MIGRATION_GUIDE.md
destinyebuka's picture
fyp
adbcca0

A newer version of the Gradio SDK is available: 6.6.0

Upgrade

πŸ”„ Authentication API Changes - Frontend Migration Guide

Date: December 24, 2024
Breaking Changes: Yes (token format changed)


Summary of Changes

The authentication system has been upgraded to a WhatsApp-style dual-token strategy for improved security. This affects login, signup, and token handling.


🚨 Breaking Changes

1. Login/Signup Response Format Changed

Before:

{
  "success": true,
  "data": {
    "user": {...},
    "token": "eyJhbG..."
  }
}

After:

{
  "success": true,
  "data": {
    "user": {...},
    "access_token": "eyJhbG...",
    "refresh_token": "eyJhbG...",
    "token_type": "bearer",
    "expires_in": 900
  }
}
Field Description
access_token Use for API calls (15 min expiry)
refresh_token Use to get new access tokens (30 day expiry)
expires_in Seconds until access_token expires

2. Token Expiry Times

Before After
Single token: 60 days Access token: 15 minutes
Refresh token: 30 days

3. OTP Length Changed

Before After
4 digits (1234) 6 digits (123456)

Update your OTP input field to accept 6 characters.


4. Password Requirements Added

Passwords must now contain:

  • βœ… At least 8 characters
  • βœ… At least 1 uppercase letter (A-Z)
  • βœ… At least 1 lowercase letter (a-z)
  • βœ… At least 1 number (0-9)

Error Response:

{
  "success": false,
  "message": "Password must contain at least 8 characters, an uppercase letter, a lowercase letter, a number"
}

πŸ†• New Endpoint: Token Refresh

POST /api/auth/refresh

Use this to get new access tokens without re-login.

Request:

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIs..."
}

Success Response (200):

{
  "success": true,
  "message": "Tokens refreshed successfully.",
  "data": {
    "access_token": "eyJhbG...",
    "refresh_token": "eyJhbG...",
    "token_type": "bearer",
    "expires_in": 900
  }
}

Error Response (401):

{
  "detail": "Invalid or expired refresh token. Please login again."
}

πŸ“± Flutter Implementation Guide

1. Update Token Storage

// OLD: Single token
await secureStorage.write(key: 'token', value: token);

// NEW: Store both tokens
await secureStorage.write(key: 'access_token', value: data['access_token']);
await secureStorage.write(key: 'refresh_token', value: data['refresh_token']);

2. Add Token Refresh Interceptor

class AuthInterceptor extends Interceptor {
  final Dio dio;
  final SecureStorage storage;
  
  AuthInterceptor(this.dio, this.storage);
  
  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final token = await storage.read(key: 'access_token');
    if (token != null) {
      options.headers['Authorization'] = 'Bearer $token';
    }
    handler.next(options);
  }
  
  @override
  void onError(DioException error, ErrorInterceptorHandler handler) async {
    if (error.response?.statusCode == 401) {
      // Access token expired - try to refresh
      final refreshed = await _refreshTokens();
      
      if (refreshed) {
        // Retry the original request with new token
        final newToken = await storage.read(key: 'access_token');
        error.requestOptions.headers['Authorization'] = 'Bearer $newToken';
        
        final response = await dio.fetch(error.requestOptions);
        return handler.resolve(response);
      } else {
        // Refresh failed - logout user
        await _logout();
      }
    }
    handler.next(error);
  }
  
  Future<bool> _refreshTokens() async {
    try {
      final refreshToken = await storage.read(key: 'refresh_token');
      if (refreshToken == null) return false;
      
      final response = await Dio().post(
        '$baseUrl/api/auth/refresh',
        data: {'refresh_token': refreshToken},
      );
      
      if (response.data['success']) {
        await storage.write(key: 'access_token', value: response.data['data']['access_token']);
        await storage.write(key: 'refresh_token', value: response.data['data']['refresh_token']);
        return true;
      }
      return false;
    } catch (e) {
      return false;
    }
  }
  
  Future<void> _logout() async {
    await storage.delete(key: 'access_token');
    await storage.delete(key: 'refresh_token');
    // Navigate to login screen
  }
}

3. Update Login Handler

Future<void> handleLogin(String identifier, String password) async {
  final response = await dio.post('/api/auth/login', data: {
    'identifier': identifier,
    'password': password,
  });
  
  if (response.data['success']) {
    final data = response.data['data'];
    
    // Store BOTH tokens
    await storage.write(key: 'access_token', value: data['access_token']);
    await storage.write(key: 'refresh_token', value: data['refresh_token']);
    
    // Store user data
    final user = User.fromJson(data['user']);
    await saveUser(user);
    
    // Navigate to home
    navigateToHome();
  }
}

4. Update Password Validation UI

String? validatePassword(String password) {
  if (password.length < 8) {
    return 'Password must be at least 8 characters';
  }
  if (!password.contains(RegExp(r'[A-Z]'))) {
    return 'Password must contain an uppercase letter';
  }
  if (!password.contains(RegExp(r'[a-z]'))) {
    return 'Password must contain a lowercase letter';
  }
  if (!password.contains(RegExp(r'[0-9]'))) {
    return 'Password must contain a number';
  }
  return null;
}

5. Update OTP Input

// Change maxLength from 4 to 6
TextField(
  controller: otpController,
  maxLength: 6,  // Changed from 4
  keyboardType: TextInputType.number,
  decoration: InputDecoration(
    hintText: '123456',  // Updated hint
  ),
)

πŸ”„ Token Flow Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      USER LOGIN                             β”‚
β”‚  POST /api/auth/login                                       β”‚
β”‚  Returns: access_token (15m) + refresh_token (30d)          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    NORMAL API CALLS                         β”‚
β”‚  Header: Authorization: Bearer <access_token>               β”‚
β”‚  β†’ API responds normally                                    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό (after 15 minutes)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              ACCESS TOKEN EXPIRES                           β”‚
β”‚  API returns 401 Unauthorized                               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              AUTOMATIC REFRESH                              β”‚
β”‚  POST /api/auth/refresh                                     β”‚
β”‚  Body: { "refresh_token": "..." }                           β”‚
β”‚  Returns: NEW access_token + NEW refresh_token              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                              β”‚
                              β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚               RETRY ORIGINAL REQUEST                        β”‚
β”‚  With new access_token β†’ Success!                           β”‚
β”‚  User never notices! ✨                                     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

⚑ Rate Limiting (New)

Auth endpoints now have rate limits to prevent brute force:

Endpoint Limit
/login 100 requests/minute per IP
/signup 100 requests/minute per IP
/verify-signup-otp 100 requests/minute per IP

429 Response:

{
  "detail": "Too many login attempts. Please try again later."
}

Handle this by showing a "Please wait" message.


βœ… Migration Checklist

  • Update login response parsing for access_token + refresh_token
  • Update signup response parsing
  • Add token refresh interceptor
  • Store both tokens securely
  • Update OTP input to 6 digits
  • Add password strength validation UI
  • Handle 429 rate limit errors
  • Test token refresh flow