Add complete OAuth 2.0 Authorization Code Flow with PKCE as alternative to API token authentication, with automatic server detection and graceful fallback to classic login. **OAuth Core (RFC 7636 PKCE):** - PKCEGenerator: S256 challenge generation for secure code exchange - OAuth DTOs: Client registration, token request/response models - OAuthClient, OAuthToken, AuthenticationMethod domain models - API.swift: registerOAuthClient() and exchangeOAuthToken() endpoints - OAuthRepository + POAuthRepository protocol **Browser Integration (ASWebAuthenticationSession):** - OAuthSession: Wraps native authentication session - OAuthFlowCoordinator: Orchestrates 5-phase OAuth flow - readeck:// URL scheme for OAuth callback handling - State verification for CSRF protection - User cancellation handling **Token Management:** - KeychainHelper: OAuth token storage alongside API tokens - TokenProvider: getOAuthToken(), setOAuthToken(), getAuthMethod() - AuthenticationMethod enum to distinguish token types - AuthRepository: loginWithOAuth(), getAuthenticationMethod() - Endpoint persistence in both Keychain and Settings **Server Feature Detection:** - ServerInfo extended with features array and supportsOAuth flag - GET /api/info endpoint integration (backward compatible) - GetServerInfoUseCase with optional endpoint parameter **User Profile Integration:** - ProfileApiClient: Fetch user data via GET /api/profile - UserProfileDto with username, email, provider information - GetUserProfileUseCase: Extract username from profile - Username saved and displayed for OAuth users (like classic auth) **Automatic OAuth Flow (No User Selection):** - OnboardingServerView: 2-phase flow (endpoint → auto-OAuth or classic) - OAuth attempted automatically if server supports it - Fallback to username/password on OAuth failure or unsupported - SettingsServerViewModel: checkServerOAuthSupport(), loginWithOAuth() **Cleanup & Refactoring:** - Remove all #if os(iOS) && !APP_EXTENSION conditionals - Remove LoginMethodSelectionView (no longer needed) - Remove switchToClassicLogin() method - Factories updated with OAuth dependencies **Testing:** - PKCEGeneratorTests: Verify RFC 7636 compliance - ServerInfoTests: Feature detection and backward compatibility - Mock implementations for all OAuth components **Documentation:** - docs/OAuth2-Implementation-Plan.md: Complete implementation guide - openapi.json: Readeck API specification **Scopes Requested:** - bookmarks:read, bookmarks:write, profile:read OAuth users now have full feature parity with classic authentication. Server auto-detects OAuth support via /info endpoint. Seamless UX with browser-based login and automatic fallback.
15 KiB
OAuth 2.0 Authentication - Implementation Plan
Date: December 19, 2025 Status: In Progress (Phase 1-4 Complete ✅) Goal: Add OAuth 2.0 authentication support as an alternative to API token authentication
📋 Overview
Current Situation (Updated)
- ✅ Phase 1-4 Complete: ServerInfo extended, OAuth core implemented, browser integration done, token management working
- ✅ Direct OAuth Flow: No login method selection - OAuth is tried automatically if server supports it
- ✅ Fallback to Classic: If OAuth fails or is not supported, classic username/password login is shown
- ✅ Endpoint Storage: Endpoint is now saved in both TokenProvider and Settings
- ❌ API Integration: API calls don't yet use OAuth tokens properly
- ❌ Testing: Manual and integration testing not complete
What's Implemented ✅
Phase 1: ServerInfo Extended
- ✅
ServerInfoDtowith nestedVersionInfostruct (canonical, release, build) - ✅
ServerInfomodel withfeatures: [String]?andsupportsOAuthcomputed property - ✅
GetServerInfoUseCasewith optional endpoint parameter - ✅
InfoApiClientwith endpoint parameter logic (custom endpoint vs TokenProvider) - ✅ Unit tests for ServerInfo and feature detection
- ✅ Backward compatibility with old servers
Phase 2: OAuth Core
- ✅
PKCEGeneratorwith verifier/challenge generation - ✅ OAuth DTOs:
OAuthClientCreateDto,OAuthClientResponseDto,OAuthTokenRequestDto,OAuthTokenResponseDto - ✅ Domain models:
OAuthClient,OAuthToken,AuthenticationMethod - ✅
API.swiftextended with OAuth methods (registerOAuthClient,exchangeOAuthToken) - ✅
OAuthRepository+POAuthRepositoryprotocol - ✅
OAuthManagerorchestrates OAuth flow - ✅ Unit tests for PKCEGenerator
Phase 3: Browser Integration
- ✅
OAuthSessionwrapsASWebAuthenticationSession - ✅
OAuthFlowCoordinatormanages 5-phase OAuth flow - ✅
readeck://URL scheme registered in Info.plist - ✅ State verification for CSRF protection
- ✅ Error handling for user cancellation
Phase 4: Token Management
- ✅
AuthenticationMethodenum (apiToken, oauth) - ✅
TokenProviderextended with OAuth methods (getOAuthToken,setOAuthToken,getAuthMethod,setAuthMethod) - ✅
setEndpoint(_ endpoint: String)added to TokenProvider - ✅
KeychainHelperextended with OAuth token storage - ✅
KeychainTokenProviderhandles both token types - ✅
AuthRepositoryextended withloginWithOAuth,getAuthenticationMethod,switchToClassicAuth - ✅ Endpoint now saved in TokenProvider (was missing before)
Phase 5: UI & UX (Partially Complete)
- ✅
OnboardingServerViewrefactored with 2-phase flow:- Phase 1: Only endpoint field + Readeck logo
- Phase 2a: OAuth attempted automatically if server supports it
- Phase 2b: Username/Password fields shown if OAuth not supported or fails
- ✅
SettingsServerViewModelextended withcheckServerOAuthSupport()andloginWithOAuth() - ✅
LoginWithOAuthUseCasecreated - ✅ Factories updated (DefaultUseCaseFactory, MockUseCaseFactory)
- ❌ LoginMethodSelectionView removed (no longer needed - OAuth is automatic)
Removed/Changed:
- ❌ All
#if os(iOS) && !APP_EXTENSIONchecks removed - ❌
LoginMethodSelectionViewremoved - OAuth is now attempted automatically - ❌
showLoginMethodSelectionflag removed - ✅ Direct OAuth attempt with fallback to classic login on error
🎯 Updated Goals
User ChoiceAutomatic OAuth: OAuth is attempted automatically if server supports it- Auto-Detection: ✅ Automatically detect if server supports OAuth via
/infoendpoint - Security: ✅ OAuth 2.0 Authorization Code Flow with PKCE (S256)
- UX: ✅ Seamless browser-based authentication using
ASWebAuthenticationSession - Fallback: ✅ Graceful fallback to username/password if OAuth fails or is not supported
- Migration Path: ❌ Not yet implemented for existing users
🔍 API Analysis
Server Info Response (Actual Format)
{
"version": {
"canonical": "0.21.4",
"release": "0.21.4",
"build": ""
},
"features": ["oauth"]
}
Changes from Original Plan:
versionis now an object, not a stringbuildDateanduserAgentremoved (not in actual response)featuresis optional (only in newer servers)
OAuth Endpoints
- Feature Detection:
GET /api/info(no auth) - Client Registration:
POST /api/oauth/client(no auth) - Authorization:
GET /authorize(browser, web page) - Token Exchange:
POST /api/oauth/token(application/x-www-form-urlencoded)
🏗️ Architecture (Updated)
Data Flow
User Input → OnboardingServerView
↓
SettingsServerViewModel.checkServerOAuthSupport()
↓
GetServerInfoUseCase.execute(endpoint: normalizedEndpoint)
↓
ServerInfoRepository.getServerInfo(endpoint: normalizedEndpoint)
↓
InfoApiClient.getServerInfo(endpoint: normalizedEndpoint)
↓
GET {endpoint}/api/info (no auth token)
↓
Response: { version: {...}, features: ["oauth"] }
↓
serverSupportsOAuth = true
↓
SettingsServerViewModel.loginWithOAuth()
↓
LoginWithOAuthUseCase.execute(endpoint)
↓
OAuthFlowCoordinator.executeOAuthFlow(endpoint)
↓
[OAuth flow: client registration → browser → token exchange]
↓
AuthRepository.loginWithOAuth(endpoint, token)
↓
TokenProvider saves: OAuthToken, AuthMethod, Endpoint
↓
User logged in ✅
Key Components
Domain Layer:
ServerInfo(version, features, supportsOAuth)OAuthClient(clientId, clientSecret, redirectUris, etc.)OAuthToken(accessToken, tokenType, scope, expiresIn, refreshToken, createdAt)AuthenticationMethodenum (apiToken, oauth)
Data Layer:
InfoApiClient- handles/api/infowith optional endpoint parameterAPI- extended with OAuth client registration and token exchangeOAuthRepository- orchestrates OAuth API callsOAuthManager- business logic for OAuth flowOAuthSession- wraps ASWebAuthenticationSessionOAuthFlowCoordinator- coordinates complete 5-phase flow
Use Cases:
GetServerInfoUseCase(endpoint: String?)- get server info with optional custom endpointLoginWithOAuthUseCase- execute OAuth login flowCheckServerReachabilityUseCase- check if server is reachable (existing)
UI Layer:
OnboardingServerView- 2-phase onboarding (endpoint → OAuth or classic)SettingsServerViewModel- extended with OAuth support
🎨 User Experience Flow (Updated)
New User Onboarding
1. User opens app
↓
2. Screen shows:
- Readeck logo (green circle)
- "Enter your Readeck server"
- Endpoint text field
- Chips: http://, https://, 192.168., :8000
- "Continue" button
↓
3. User enters endpoint (e.g., https://readeck.example.com)
↓
4. User taps "Continue"
↓
5. App normalizes endpoint and calls /api/info
↓
6a. If OAuth supported:
↓
App automatically starts OAuth flow
↓
Browser opens with /authorize page
↓
User logs in with username/password on server
↓
User approves permissions
↓
Redirect to readeck://oauth-callback?code=...
↓
App exchanges code for token
↓
Token + endpoint saved
↓
User logged in ✅
6b. If OAuth NOT supported OR OAuth fails:
↓
Username and Password fields appear
↓
Text changes to "Enter your credentials"
↓
Button changes to "Login & Save"
↓
User enters credentials
↓
Classic API token login
↓
User logged in ✅
Key UX Changes:
- No login method selection screen (removed)
- OAuth is attempted automatically
- Fallback to classic is seamless
- User sees endpoint field first, login fields only if needed
🚀 Implementation Status
✅ Phase 1: ServerInfo Extended (COMPLETE)
- ✅
ServerInfoDtowithVersionInfonested struct - ✅
ServerInfo.featuresoptional array - ✅
supportsOAuthcomputed property - ✅
GetServerInfoUseCasewith endpoint parameter - ✅ Backward compatibility tests
- ✅ Mock implementations updated
✅ Phase 2: OAuth Core (COMPLETE)
- ✅ PKCEGenerator (verifier + challenge)
- ✅ All OAuth DTOs created
- ✅ Domain models created
- ✅ API methods implemented
- ✅ OAuthRepository created
- ✅ OAuthManager created
- ✅ Unit tests for PKCE
✅ Phase 3: Browser Integration (COMPLETE)
- ✅ OAuthSession wraps ASWebAuthenticationSession
- ✅ OAuthFlowCoordinator manages complete flow
- ✅ URL scheme registered (readeck://)
- ✅ State verification
- ✅ Error handling
✅ Phase 4: Token Management (COMPLETE)
- ✅ AuthenticationMethod enum
- ✅ TokenProvider extended with OAuth methods
- ✅
setEndpoint()added to TokenProvider - ✅ KeychainHelper extended
- ✅ Token type detection in getToken()
- ✅ AuthRepository extended
- ✅ Endpoint saved in both TokenProvider and Settings
🟡 Phase 5: Use Cases & ViewModels (MOSTLY COMPLETE)
- ✅ LoginWithOAuthUseCase created
- ✅ AuthRepository extended
- ✅ SettingsServerViewModel extended
- ✅ Factories updated
- ❌ LoginMethodSelectionView removed (design change)
- ❌ Integration tests not complete
🟡 Phase 6: UI Implementation (MOSTLY COMPLETE)
- ✅ OnboardingServerView refactored (2-phase flow)
- ✅ Readeck logo with green background
- ✅ Dynamic text based on phase
- ✅ Conditional login fields
- ✅ OAuth auto-attempt logic
- ❌ Settings migration UI not implemented
- ❌ OAuth token info display not implemented
❌ Phase 7: Testing & Polish (NOT STARTED)
- ❌ Manual testing checklist
- ❌ Integration tests
- ❌ Performance testing
- ❌ Code review
❌ Phase 8: Migration & Documentation (NOT STARTED)
- ❌ Migration flow for existing users
- ❌ Release notes
- ❌ Documentation
🐛 Known Issues & TODOs
Critical Issues
-
API Calls Don't Use OAuth Token Properly
- Problem: When using OAuth, API calls may not be sending the Bearer token correctly
- The
TokenProvider.getToken()checks auth method and returns OAuth access token - But need to verify API calls actually use it in Authorization header
- Check if API.swift properly uses
await tokenProvider.getToken()for all authenticated requests
-
Endpoint Parameter Flow Needs Verification
GetServerInfoUseCase.execute(endpoint:)receives endpoint parameterServerInfoRepository.getServerInfo(endpoint:)passes it throughInfoApiClient.getServerInfo(endpoint:)uses it OR falls back to TokenProvider- Need to verify this chain works correctly
Minor Issues
-
No Migration UI for Existing Users
- Users with API tokens can't migrate to OAuth yet
- Settings screen needs "Switch to OAuth" button
-
No OAuth Token Display
- Can't see which auth method is active
- Can't see token expiry or scopes
-
All
#ifChecks Removed- Removed all
#if os(iOS) && !APP_EXTENSIONconditionals - May cause issues with URLShare extension (needs verification)
- Removed all
✅ Updated TODO List
🔴 IMMEDIATE (Critical for functionality)
-
Verify API Integration with OAuth Token
- Check that
API.swiftusesawait tokenProvider.getToken()in all authenticated requests - Verify Bearer token is sent correctly in Authorization header
- Test actual API call (e.g., fetch bookmarks) with OAuth token
- Add logging to confirm which token type is being used
- Check that
-
Test Complete OAuth Flow End-to-End
- Fresh install → OAuth server → OAuth login → fetch data
- Fresh install → non-OAuth server → classic login → fetch data
- OAuth login failure → fallback to classic login
- Verify endpoint is saved correctly in TokenProvider
🟡 HIGH PRIORITY (Important for UX)
-
Settings Screen Migration
- Show current authentication method (OAuth vs API Token)
- Add "Switch to OAuth" button (if server supports it and user has API token)
- Add "Switch to Classic Login" option
- Show OAuth token info (scopes, expiry if available)
-
Error Handling & Messages
- Better error messages for OAuth failures
- Network error handling
- Token expiry handling
- Server not reachable scenarios
🟢 MEDIUM PRIORITY (Nice to have)
-
Testing
- Manual testing on real device
- Integration tests for OAuth flow
- Test on different iOS versions
- Test with different server versions
-
Documentation
- Update README with OAuth instructions
- Add inline code documentation
- Create user guide for OAuth setup
⚪ LOW PRIORITY (Future enhancements)
- Token refresh support (if server implements it)
- Revoke token on logout
- Multiple account support
- Biometric re-authentication
📚 Implementation Notes
Design Decisions
-
No Login Method Selection Screen
- Original plan had LoginMethodSelectionView
- Removed in favor of automatic OAuth attempt
- Rationale: Better UX, fewer clicks, OAuth is preferred
- Fallback to classic login happens automatically on error
-
Endpoint in TokenProvider
- Endpoint now saved in BOTH TokenProvider (Keychain) and Settings
- Rationale: InfoApiClient needs endpoint from TokenProvider for /api/info calls
- Ensures endpoint is available for all API calls
-
Removed All
#if os(iOS) && !APP_EXTENSION- Original code had many conditional compilation checks
- All removed for cleaner code
- May need to revisit if URLShare extension has issues
-
ServerInfoDto Structure Changed
- Original plan had flat version string
- Actual API returns nested VersionInfo object
- Adapted to match real API response
Code Patterns
Endpoint Resolution:
// InfoApiClient
func getServerInfo(endpoint: String? = nil) async throws -> ServerInfoDto {
let baseEndpoint = try await resolveEndpoint(endpoint)
// Uses provided endpoint OR falls back to tokenProvider.getEndpoint()
}
Token Type Detection:
// KeychainTokenProvider
func getToken() async -> String? {
if let method = await getAuthMethod(), method == .oauth {
return await getOAuthToken()?.accessToken
}
return keychainHelper.loadToken() // API token
}
OAuth Flow:
// 5-phase flow in OAuthFlowCoordinator
1. Register client + generate PKCE
2. Build authorization URL
3. Open browser (ASWebAuthenticationSession)
4. Parse callback URL
5. Exchange code for token
🎯 Next Steps
For Next Development Session
-
Verify API Integration (Most Critical)
- Add logging to see which token is being used
- Test actual API call with OAuth token
- Confirm Bearer token in Authorization header
-
Manual Testing
- Test OAuth flow end-to-end
- Test fallback to classic login
- Test on real device
-
Settings Screen
- Show current auth method
- Add migration button
-
Error Handling
- Better error messages
- Handle all failure scenarios
Last Updated: December 19, 2025 Status: Phase 1-4 Complete, Phase 5-6 Mostly Complete, Testing Needed