diff --git a/openspec/changes/migrate-to-external-api-authentication/proposal.md b/openspec/changes/migrate-to-external-api-authentication/proposal.md new file mode 100644 index 0000000..98939b0 --- /dev/null +++ b/openspec/changes/migrate-to-external-api-authentication/proposal.md @@ -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 \ No newline at end of file diff --git a/openspec/changes/migrate-to-external-api-authentication/tasks.md b/openspec/changes/migrate-to-external-api-authentication/tasks.md new file mode 100644 index 0000000..50fb123 --- /dev/null +++ b/openspec/changes/migrate-to-external-api-authentication/tasks.md @@ -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 \ No newline at end of file diff --git a/openspec/changes/migrate-to-external-api-authentication/test_external_auth.py b/openspec/changes/migrate-to-external-api-authentication/test_external_auth.py new file mode 100644 index 0000000..b1b059a --- /dev/null +++ b/openspec/changes/migrate-to-external-api-authentication/test_external_auth.py @@ -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() \ No newline at end of file