proposal: migrate to external API authentication
Create OpenSpec proposal for migrating from local database authentication to external API authentication using Microsoft Azure AD. Changes proposed: - Replace local username/password auth with external API - Integrate with https://pj-auth-api.vercel.app/api/auth/login - Use Azure AD tokens instead of local JWT - Display user 'name' from API response in UI - Maintain backward compatibility with feature flag Benefits: - Single Sign-On (SSO) capability - Leverage enterprise identity management - Reduce local user management overhead - Consistent authentication across applications Database changes: - Add external_user_id for Azure AD user mapping - Add display_name for UI display - Keep existing schema for rollback capability Implementation includes: - Detailed migration plan with phased rollout - Comprehensive task list for implementation - Test script for API validation - Risk assessment and mitigation strategies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
# Change: Migrate to External API Authentication
|
||||
|
||||
## Why
|
||||
|
||||
The current local database authentication system has several limitations:
|
||||
- User credentials are managed locally, requiring manual user creation and password management
|
||||
- No centralized authentication with enterprise identity systems
|
||||
- Cannot leverage existing enterprise authentication infrastructure (e.g., Microsoft Azure AD)
|
||||
- No single sign-on (SSO) capability
|
||||
- Increased maintenance overhead for user management
|
||||
|
||||
By migrating to the external API authentication service at https://pj-auth-api.vercel.app, the system will:
|
||||
- Integrate with enterprise Microsoft Azure AD authentication
|
||||
- Enable single sign-on (SSO) for users
|
||||
- Eliminate local password management
|
||||
- Leverage existing enterprise user management and security policies
|
||||
- Reduce maintenance overhead
|
||||
- Provide consistent authentication across multiple applications
|
||||
|
||||
## What Changes
|
||||
|
||||
### Authentication Flow
|
||||
- **Current**: Local database authentication using username/password stored in MySQL
|
||||
- **New**: External API authentication via POST to `https://pj-auth-api.vercel.app/api/auth/login`
|
||||
- **Token Management**: Use JWT tokens from external API instead of locally generated tokens
|
||||
- **User Display**: Use `name` field from API response for user display instead of local username
|
||||
|
||||
### API Integration
|
||||
**Endpoint**: `POST https://pj-auth-api.vercel.app/api/auth/login`
|
||||
|
||||
**Request Format**:
|
||||
```json
|
||||
{
|
||||
"username": "user@domain.com",
|
||||
"password": "user_password"
|
||||
}
|
||||
```
|
||||
|
||||
**Success Response (200)**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "認證成功",
|
||||
"data": {
|
||||
"access_token": "eyJ0eXAiOiJKV1Q...",
|
||||
"id_token": "eyJ0eXAiOiJKV1Q...",
|
||||
"expires_in": 4999,
|
||||
"token_type": "Bearer",
|
||||
"userInfo": {
|
||||
"id": "42cf0b98-f598-47dd-ae2a-f33803f87d41",
|
||||
"name": "ymirliu 劉念萱",
|
||||
"email": "ymirliu@panjit.com.tw",
|
||||
"jobTitle": null,
|
||||
"officeLocation": "高雄",
|
||||
"businessPhones": ["1580"]
|
||||
},
|
||||
"issuedAt": "2025-11-14T07:09:15.203Z",
|
||||
"expiresAt": "2025-11-14T08:32:34.203Z"
|
||||
},
|
||||
"timestamp": "2025-11-14T07:09:15.203Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Failure Response (401)**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "用戶名或密碼錯誤",
|
||||
"code": "INVALID_CREDENTIALS",
|
||||
"timestamp": "2025-11-14T07:10:02.585Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Database Schema Changes
|
||||
- **users table modifications**:
|
||||
- Remove/deprecate `hashed_password` column (keep for rollback)
|
||||
- Add `external_user_id` (VARCHAR 255) - Store Azure AD user ID
|
||||
- Add `display_name` (VARCHAR 255) - Store user display name from API
|
||||
- Add `azure_email` (VARCHAR 255) - Store Azure AD email
|
||||
- Add `last_token_refresh` (DATETIME) - Track token refresh timing
|
||||
- Keep `username` for backward compatibility (can be email)
|
||||
|
||||
### Session Management
|
||||
- Store external API tokens in session/cache instead of local JWT
|
||||
- Implement token refresh mechanism based on `expires_in` field
|
||||
- Use `expiresAt` timestamp for token expiration validation
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected Capabilities
|
||||
- `authentication`: Complete replacement of authentication mechanism
|
||||
- `user-management`: Simplified to read-only user information from external API
|
||||
- `session-management`: Modified to handle external tokens
|
||||
|
||||
### Affected Code
|
||||
- **Backend Authentication**:
|
||||
- `backend/app/api/v1/endpoints/auth.py`: Replace login logic with external API call
|
||||
- `backend/app/core/security.py`: Modify token validation to use external tokens
|
||||
- `backend/app/core/auth.py`: Update authentication dependencies
|
||||
- `backend/app/services/auth_service.py`: New service for external API integration
|
||||
|
||||
- **Database Models**:
|
||||
- `backend/app/models/user.py`: Update User model with new fields
|
||||
- `backend/alembic/versions/`: New migration for schema changes
|
||||
|
||||
- **Frontend**:
|
||||
- `frontend/src/services/authService.ts`: Update to handle new token format
|
||||
- `frontend/src/stores/authStore.ts`: Modify to store/display user info from API
|
||||
- `frontend/src/components/Header.tsx`: Display `name` field instead of username
|
||||
|
||||
### Dependencies
|
||||
- Add `httpx` or `aiohttp` for async HTTP requests to external API (already present)
|
||||
- No new package dependencies required
|
||||
|
||||
### Configuration
|
||||
- New environment variables:
|
||||
- `EXTERNAL_AUTH_API_URL` = "https://pj-auth-api.vercel.app"
|
||||
- `EXTERNAL_AUTH_ENDPOINT` = "/api/auth/login"
|
||||
- `EXTERNAL_AUTH_TIMEOUT` = 30 (seconds)
|
||||
- `USE_EXTERNAL_AUTH` = true (feature flag for gradual rollout)
|
||||
- `TOKEN_REFRESH_BUFFER` = 300 (refresh tokens 5 minutes before expiry)
|
||||
|
||||
### Security Considerations
|
||||
- HTTPS required for all authentication requests
|
||||
- Token storage must be secure (HTTPOnly cookies or secure session storage)
|
||||
- Implement rate limiting for authentication attempts
|
||||
- Log all authentication events for audit trail
|
||||
- Validate SSL certificates for external API calls
|
||||
- Handle network failures gracefully with appropriate error messages
|
||||
|
||||
### Rollback Strategy
|
||||
- Keep existing authentication code with feature flag
|
||||
- Maintain password column in database (don't drop immediately)
|
||||
- Implement dual authentication mode during transition:
|
||||
- If `USE_EXTERNAL_AUTH=true`: Use external API
|
||||
- If `USE_EXTERNAL_AUTH=false`: Use local authentication
|
||||
- Provide migration script to sync existing users with external system
|
||||
|
||||
### Migration Plan
|
||||
1. **Phase 1**: Implement external API authentication alongside existing system
|
||||
2. **Phase 2**: Test with subset of users (based on domain or user flag)
|
||||
3. **Phase 3**: Gradual rollout to all users
|
||||
4. **Phase 4**: Deprecate local authentication (keep code for emergency)
|
||||
5. **Phase 5**: Remove local authentication code (after stable period)
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
### Risks
|
||||
1. **External API Unavailability**: Authentication service downtime blocks all logins
|
||||
- *Mitigation*: Implement fallback to local auth, cache tokens, implement retry logic
|
||||
|
||||
2. **Token Expiration Handling**: Users may be logged out unexpectedly
|
||||
- *Mitigation*: Implement automatic token refresh before expiration
|
||||
|
||||
3. **Network Latency**: Slower authentication due to external API calls
|
||||
- *Mitigation*: Implement proper timeout handling, async requests, response caching
|
||||
|
||||
4. **Data Consistency**: User information mismatch between local DB and external system
|
||||
- *Mitigation*: Regular sync jobs, use external system as single source of truth
|
||||
|
||||
5. **Breaking Change**: Existing sessions will be invalidated
|
||||
- *Mitigation*: Provide migration window, clear communication to users
|
||||
|
||||
## Success Criteria
|
||||
- All users can authenticate via external API
|
||||
- Authentication response time < 2 seconds (95th percentile)
|
||||
- Zero data loss during migration
|
||||
- Automatic token refresh works without user intervention
|
||||
- Proper error messages for all failure scenarios
|
||||
- Audit logs capture all authentication events
|
||||
- Rollback procedure tested and documented
|
||||
180
openspec/changes/migrate-to-external-api-authentication/tasks.md
Normal file
180
openspec/changes/migrate-to-external-api-authentication/tasks.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Implementation Tasks
|
||||
|
||||
## 1. Database Schema Updates
|
||||
- [ ] 1.1 Create database migration script
|
||||
- Add `external_user_id` column (VARCHAR 255)
|
||||
- Add `display_name` column (VARCHAR 255)
|
||||
- Add `azure_email` column (VARCHAR 255)
|
||||
- Add `last_token_refresh` column (DATETIME)
|
||||
- Mark `hashed_password` as nullable (for gradual migration)
|
||||
- [ ] 1.2 Update User model
|
||||
- Add new fields to SQLAlchemy model
|
||||
- Update model relationships if needed
|
||||
- Add migration version with Alembic
|
||||
- [ ] 1.3 Create user sync mechanism
|
||||
- Script to map existing users to external IDs
|
||||
- Handle users without external accounts
|
||||
- Backup existing user data
|
||||
|
||||
## 2. Configuration Management
|
||||
- [ ] 2.1 Update environment configuration
|
||||
- Add `EXTERNAL_AUTH_API_URL` to `.env.local`
|
||||
- Add `EXTERNAL_AUTH_ENDPOINT` configuration
|
||||
- Add `EXTERNAL_AUTH_TIMEOUT` setting
|
||||
- Add `USE_EXTERNAL_AUTH` feature flag
|
||||
- Add `TOKEN_REFRESH_BUFFER` setting
|
||||
- [ ] 2.2 Update Settings class
|
||||
- Add external auth settings to `backend/app/core/config.py`
|
||||
- Add validation for new configuration values
|
||||
- Implement feature flag logic
|
||||
|
||||
## 3. External API Integration Service
|
||||
- [ ] 3.1 Create auth API client
|
||||
- Implement `backend/app/services/external_auth_service.py`
|
||||
- Create async HTTP client for API calls
|
||||
- Implement request/response models
|
||||
- Add proper error handling and logging
|
||||
- [ ] 3.2 Implement authentication methods
|
||||
- `authenticate_user()` - Call external API
|
||||
- `validate_token()` - Verify token validity
|
||||
- `refresh_token()` - Handle token refresh
|
||||
- `get_user_info()` - Fetch user details
|
||||
- [ ] 3.3 Add resilience patterns
|
||||
- Implement retry logic with exponential backoff
|
||||
- Add circuit breaker pattern
|
||||
- Implement timeout handling
|
||||
- Add fallback mechanisms
|
||||
|
||||
## 4. Backend Authentication Updates
|
||||
- [ ] 4.1 Modify login endpoint
|
||||
- Update `backend/app/api/v1/endpoints/auth.py`
|
||||
- Route to external API based on feature flag
|
||||
- Handle both authentication modes during transition
|
||||
- Return appropriate token format
|
||||
- [ ] 4.2 Update token validation
|
||||
- Modify `backend/app/core/security.py`
|
||||
- Support both local and external tokens
|
||||
- Implement token type detection
|
||||
- Update JWT validation logic
|
||||
- [ ] 4.3 Update authentication dependencies
|
||||
- Modify `backend/app/core/auth.py`
|
||||
- Update `get_current_user()` dependency
|
||||
- Handle external user information
|
||||
- Implement proper user context
|
||||
|
||||
## 5. Session and Token Management
|
||||
- [ ] 5.1 Implement token storage
|
||||
- Store external tokens securely
|
||||
- Implement token encryption at rest
|
||||
- Handle multiple token types (access, ID, refresh)
|
||||
- [ ] 5.2 Create token refresh mechanism
|
||||
- Background task for token refresh
|
||||
- Refresh tokens before expiration
|
||||
- Update stored tokens atomically
|
||||
- Handle refresh failures gracefully
|
||||
- [ ] 5.3 Session invalidation
|
||||
- Clear tokens on logout
|
||||
- Handle token revocation
|
||||
- Implement session timeout
|
||||
|
||||
## 6. Frontend Updates
|
||||
- [ ] 6.1 Update authentication service
|
||||
- Modify `frontend/src/services/authService.ts`
|
||||
- Handle new token format
|
||||
- Store user display information
|
||||
- Implement token refresh on client side
|
||||
- [ ] 6.2 Update auth store
|
||||
- Modify `frontend/src/stores/authStore.ts`
|
||||
- Store external user information
|
||||
- Update user display logic
|
||||
- Handle token expiration
|
||||
- [ ] 6.3 Update UI components
|
||||
- Modify `frontend/src/components/Header.tsx`
|
||||
- Display user `name` instead of username
|
||||
- Show additional user information
|
||||
- Update login form if needed
|
||||
- [ ] 6.4 Error handling
|
||||
- Handle external API errors
|
||||
- Display appropriate error messages
|
||||
- Implement retry UI for failures
|
||||
- Add loading states
|
||||
|
||||
## 7. Testing
|
||||
- [ ] 7.1 Unit tests
|
||||
- Test external auth service
|
||||
- Test token validation
|
||||
- Test user information mapping
|
||||
- Test error scenarios
|
||||
- [ ] 7.2 Integration tests
|
||||
- Test full authentication flow
|
||||
- Test token refresh mechanism
|
||||
- Test fallback scenarios
|
||||
- Test feature flag switching
|
||||
- [ ] 7.3 Load testing
|
||||
- Test external API response times
|
||||
- Test system under high authentication load
|
||||
- Measure impact on performance
|
||||
- [ ] 7.4 Security testing
|
||||
- Test token security
|
||||
- Verify HTTPS enforcement
|
||||
- Test rate limiting
|
||||
- Validate error message security
|
||||
|
||||
## 8. Migration Execution
|
||||
- [ ] 8.1 Pre-migration preparation
|
||||
- Backup database
|
||||
- Document rollback procedure
|
||||
- Prepare user communication
|
||||
- Set up monitoring
|
||||
- [ ] 8.2 Staged rollout
|
||||
- Enable for test users first
|
||||
- Monitor for issues
|
||||
- Gradually increase user percentage
|
||||
- Collect feedback
|
||||
- [ ] 8.3 Post-migration validation
|
||||
- Verify all users can login
|
||||
- Check audit logs
|
||||
- Monitor error rates
|
||||
- Validate performance metrics
|
||||
|
||||
## 9. Documentation
|
||||
- [ ] 9.1 Technical documentation
|
||||
- Update API documentation
|
||||
- Document authentication flow
|
||||
- Update deployment guide
|
||||
- Create troubleshooting guide
|
||||
- [ ] 9.2 User documentation
|
||||
- Update login instructions
|
||||
- Document new features
|
||||
- Create FAQ for common issues
|
||||
- [ ] 9.3 Operations documentation
|
||||
- Document monitoring points
|
||||
- Create runbook for issues
|
||||
- Document rollback procedure
|
||||
|
||||
## 10. Monitoring and Observability
|
||||
- [ ] 10.1 Add monitoring metrics
|
||||
- Authentication success/failure rates
|
||||
- External API response times
|
||||
- Token refresh success rate
|
||||
- Error rate monitoring
|
||||
- [ ] 10.2 Implement logging
|
||||
- Log all authentication attempts
|
||||
- Log external API calls
|
||||
- Log token operations
|
||||
- Structured logging for analysis
|
||||
- [ ] 10.3 Create alerts
|
||||
- Alert on high failure rates
|
||||
- Alert on external API unavailability
|
||||
- Alert on token refresh failures
|
||||
- Alert on unusual patterns
|
||||
|
||||
## 11. Cleanup (Post-Stabilization)
|
||||
- [ ] 11.1 Remove legacy code
|
||||
- Remove local authentication code (after stable period)
|
||||
- Remove unused database columns
|
||||
- Clean up configuration
|
||||
- [ ] 11.2 Optimize performance
|
||||
- Implement caching where appropriate
|
||||
- Optimize database queries
|
||||
- Review and optimize API calls
|
||||
@@ -0,0 +1,226 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Proof of Concept: External API Authentication Test
|
||||
Tests the external authentication API at https://pj-auth-api.vercel.app
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
import httpx
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""User information from external API"""
|
||||
id: str
|
||||
name: str
|
||||
email: str
|
||||
job_title: Optional[str] = Field(None, alias="jobTitle")
|
||||
office_location: Optional[str] = Field(None, alias="officeLocation")
|
||||
business_phones: list[str] = Field(default_factory=list, alias="businessPhones")
|
||||
|
||||
|
||||
class AuthSuccessData(BaseModel):
|
||||
"""Successful authentication response data"""
|
||||
access_token: str
|
||||
id_token: str
|
||||
expires_in: int
|
||||
token_type: str
|
||||
user_info: UserInfo = Field(alias="userInfo")
|
||||
issued_at: str = Field(alias="issuedAt")
|
||||
expires_at: str = Field(alias="expiresAt")
|
||||
|
||||
|
||||
class AuthSuccessResponse(BaseModel):
|
||||
"""Successful authentication response"""
|
||||
success: bool
|
||||
message: str
|
||||
data: AuthSuccessData
|
||||
timestamp: str
|
||||
|
||||
|
||||
class AuthErrorResponse(BaseModel):
|
||||
"""Failed authentication response"""
|
||||
success: bool
|
||||
error: str
|
||||
code: str
|
||||
timestamp: str
|
||||
|
||||
|
||||
class ExternalAuthClient:
|
||||
"""Client for external authentication API"""
|
||||
|
||||
def __init__(self, base_url: str = "https://pj-auth-api.vercel.app", timeout: int = 30):
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self.endpoint = "/api/auth/login"
|
||||
|
||||
async def authenticate(self, username: str, password: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Authenticate user with external API
|
||||
|
||||
Args:
|
||||
username: User email/username
|
||||
password: User password
|
||||
|
||||
Returns:
|
||||
Authentication result dictionary
|
||||
"""
|
||||
url = f"{self.base_url}{self.endpoint}"
|
||||
|
||||
print(f"ℹ Endpoint: POST {url}")
|
||||
print(f"ℹ Username: {username}")
|
||||
print(f"ℹ Timestamp: {datetime.now().isoformat()}")
|
||||
print()
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
try:
|
||||
# Make authentication request
|
||||
start_time = datetime.now()
|
||||
response = await client.post(
|
||||
url,
|
||||
json={"username": username, "password": password},
|
||||
timeout=self.timeout
|
||||
)
|
||||
elapsed = (datetime.now() - start_time).total_seconds()
|
||||
|
||||
# Print response details
|
||||
print("Response Details:")
|
||||
print(f" Status Code: {response.status_code}")
|
||||
print(f" Response Time: {elapsed:.3f}s")
|
||||
print(f" Content-Type: {response.headers.get('content-type', 'N/A')}")
|
||||
print()
|
||||
|
||||
# Parse response
|
||||
response_data = response.json()
|
||||
print("Response Body:")
|
||||
print(json.dumps(response_data, indent=2, ensure_ascii=False))
|
||||
print()
|
||||
|
||||
# Handle success/failure
|
||||
if response.status_code == 200:
|
||||
auth_response = AuthSuccessResponse(**response_data)
|
||||
return {
|
||||
"success": True,
|
||||
"status_code": response.status_code,
|
||||
"data": auth_response.dict(),
|
||||
"user_display_name": auth_response.data.user_info.name,
|
||||
"user_email": auth_response.data.user_info.email,
|
||||
"token": auth_response.data.access_token,
|
||||
"expires_in": auth_response.data.expires_in,
|
||||
"expires_at": auth_response.data.expires_at
|
||||
}
|
||||
elif response.status_code == 401:
|
||||
error_response = AuthErrorResponse(**response_data)
|
||||
return {
|
||||
"success": False,
|
||||
"status_code": response.status_code,
|
||||
"error": error_response.error,
|
||||
"code": error_response.code
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"status_code": response.status_code,
|
||||
"error": f"Unexpected status code: {response.status_code}",
|
||||
"response": response_data
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
print(f"❌ Request timeout after {self.timeout} seconds")
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Request timeout",
|
||||
"code": "TIMEOUT"
|
||||
}
|
||||
except httpx.RequestError as e:
|
||||
print(f"❌ Request error: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"code": "REQUEST_ERROR"
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e),
|
||||
"code": "UNKNOWN_ERROR"
|
||||
}
|
||||
|
||||
|
||||
async def test_authentication():
|
||||
"""Test authentication with different scenarios"""
|
||||
client = ExternalAuthClient()
|
||||
|
||||
# Test scenarios
|
||||
test_cases = [
|
||||
{
|
||||
"name": "Valid Credentials (Example)",
|
||||
"username": "ymirliu@panjit.com.tw",
|
||||
"password": "correct_password", # Replace with actual password for testing
|
||||
"expected": "success"
|
||||
},
|
||||
{
|
||||
"name": "Invalid Credentials",
|
||||
"username": "test@example.com",
|
||||
"password": "wrong_password",
|
||||
"expected": "failure"
|
||||
}
|
||||
]
|
||||
|
||||
for i, test_case in enumerate(test_cases, 1):
|
||||
print(f"{'='*60}")
|
||||
print(f"Test Case {i}: {test_case['name']}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
result = await client.authenticate(
|
||||
username=test_case["username"],
|
||||
password=test_case["password"]
|
||||
)
|
||||
|
||||
# Analyze result
|
||||
print("\nAnalysis:")
|
||||
if result["success"]:
|
||||
print("✅ Authentication successful")
|
||||
print(f" User: {result.get('user_display_name', 'N/A')}")
|
||||
print(f" Email: {result.get('user_email', 'N/A')}")
|
||||
print(f" Token expires in: {result.get('expires_in', 0)} seconds")
|
||||
print(f" Expires at: {result.get('expires_at', 'N/A')}")
|
||||
else:
|
||||
print("❌ Authentication failed")
|
||||
print(f" Error: {result.get('error', 'Unknown error')}")
|
||||
print(f" Code: {result.get('code', 'N/A')}")
|
||||
|
||||
print("\n")
|
||||
|
||||
|
||||
async def test_token_validation():
|
||||
"""Test token validation and refresh logic"""
|
||||
# This would be implemented when we have a valid token
|
||||
print("Token validation test - To be implemented with actual tokens")
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point"""
|
||||
print("External Authentication API Test")
|
||||
print("================================\n")
|
||||
|
||||
# Run tests
|
||||
asyncio.run(test_authentication())
|
||||
|
||||
print("\nTest completed!")
|
||||
print("\nNotes for implementation:")
|
||||
print("1. Use httpx for async HTTP requests (already in requirements)")
|
||||
print("2. Store tokens securely (consider encryption)")
|
||||
print("3. Implement automatic token refresh before expiration")
|
||||
print("4. Handle network failures with retry logic")
|
||||
print("5. Map external user ID to local user records")
|
||||
print("6. Display user 'name' field in UI instead of username")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user