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:
egg
2025-11-14 15:14:48 +08:00
parent b048f2d640
commit 28e419f5fa
3 changed files with 577 additions and 0 deletions

View File

@@ -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

View 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

View File

@@ -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()