Spaces:
Running
Running
| # π 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:** | |
| ```json | |
| { | |
| "success": true, | |
| "data": { | |
| "user": {...}, | |
| "token": "eyJhbG..." | |
| } | |
| } | |
| ``` | |
| **After:** | |
| ```json | |
| { | |
| "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:** | |
| ```json | |
| { | |
| "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:** | |
| ```json | |
| { | |
| "refresh_token": "eyJhbGciOiJIUzI1NiIs..." | |
| } | |
| ``` | |
| **Success Response (200):** | |
| ```json | |
| { | |
| "success": true, | |
| "message": "Tokens refreshed successfully.", | |
| "data": { | |
| "access_token": "eyJhbG...", | |
| "refresh_token": "eyJhbG...", | |
| "token_type": "bearer", | |
| "expires_in": 900 | |
| } | |
| } | |
| ``` | |
| **Error Response (401):** | |
| ```json | |
| { | |
| "detail": "Invalid or expired refresh token. Please login again." | |
| } | |
| ``` | |
| --- | |
| ## π± Flutter Implementation Guide | |
| ### 1. Update Token Storage | |
| ```dart | |
| // 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 | |
| ```dart | |
| 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 | |
| ```dart | |
| 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 | |
| ```dart | |
| 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 | |
| ```dart | |
| // 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:** | |
| ```json | |
| { | |
| "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 | |